├── .github └── FUNDING.yml ├── demo ├── img │ ├── swim.jpg │ ├── mountain-biking.jpg │ ├── power-analysis.jpg │ └── quadrant-analysis.jpg ├── fit_files │ ├── swim.fit │ ├── road-cycling.fit │ ├── power-analysis.fit │ └── mountain-biking.fit ├── index.php ├── libraries │ ├── PolylineEncoder.php │ └── Line_DouglasPeucker.php ├── css │ └── dc.css ├── swim.php ├── mountain-biking-leaflet.php ├── js │ ├── jquery.flot.pie.min.js │ └── jquery.flot.min.js ├── mountain-biking.php ├── quadrant-analysis.php └── power-analysis.php ├── utils ├── Profile.xlsx └── readProfileCSV.php ├── .codeclimate.yml ├── .travis.yml ├── .gitattributes ├── composer.json ├── .gitignore ├── tests ├── pFFA-EnumData-Test.php ├── pFFA-IsPaused-Test.php ├── pFFA-GetJSON-Test.php ├── pFFA-HR-Test.php ├── pFFA-QuadrantAnalysis-Test.php ├── pFFA-SetUnits-Test.php ├── pFFA-Basic-Test.php ├── pFFA-FixData-Test.php └── pFFA-Power-Test.php ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adriangibbons 2 | -------------------------------------------------------------------------------- /demo/img/swim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/img/swim.jpg -------------------------------------------------------------------------------- /utils/Profile.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/utils/Profile.xlsx -------------------------------------------------------------------------------- /demo/fit_files/swim.fit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/fit_files/swim.fit -------------------------------------------------------------------------------- /demo/img/mountain-biking.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/img/mountain-biking.jpg -------------------------------------------------------------------------------- /demo/img/power-analysis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/img/power-analysis.jpg -------------------------------------------------------------------------------- /demo/fit_files/road-cycling.fit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/fit_files/road-cycling.fit -------------------------------------------------------------------------------- /demo/img/quadrant-analysis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/img/quadrant-analysis.jpg -------------------------------------------------------------------------------- /demo/fit_files/power-analysis.fit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/fit_files/power-analysis.fit -------------------------------------------------------------------------------- /demo/fit_files/mountain-biking.fit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adriangibbons/php-fit-file-analysis/HEAD/demo/fit_files/mountain-biking.fit -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | phpmd: 3 | enabled: true 4 | 5 | ratings: 6 | paths: 7 | - "**.php" 8 | 9 | exclude_paths: 10 | - "demo/" 11 | - "tests/" 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: php 3 | php: 4 | - 5.5 5 | 6 | before_script: 7 | - curl -s http://getcomposer.org/installer | php 8 | - php composer.phar install --dev --no-interaction 9 | 10 | script: 11 | - mkdir -p build/logs 12 | - phpunit --coverage-clover build/logs/clover.xml tests 13 | 14 | after_script: 15 | - php vendor/bin/coveralls -v 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adriangibbons/php-fit-file-analysis", 3 | "type": "library", 4 | "description": "A PHP class for analysing FIT files created by Garmin GPS devices", 5 | "keywords": ["garmin", "fit"], 6 | "homepage": "https://github.com/adriangibbons/php-fit-file-analysis", 7 | "require-dev": { 8 | "phpunit/phpunit": "4.8.*", 9 | "squizlabs/php_codesniffer": "2.*", 10 | "satooshi/php-coveralls": "^2.0" 11 | }, 12 | "autoload": { 13 | "psr-4": { 14 | "adriangibbons\\": "src/" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | issues 3 | composer.phar 4 | composer.lock 5 | 6 | # Windows image file caches 7 | Thumbs.db 8 | ehthumbs.db 9 | 10 | # Folder config file 11 | Desktop.ini 12 | 13 | # Recycle Bin used on file shares 14 | $RECYCLE.BIN/ 15 | 16 | # Windows Installer files 17 | *.cab 18 | *.msi 19 | *.msm 20 | *.msp 21 | 22 | # Windows shortcuts 23 | *.lnk 24 | 25 | # ========================= 26 | # Operating System Files 27 | # ========================= 28 | 29 | # OSX 30 | # ========================= 31 | 32 | .DS_Store 33 | .AppleDouble 34 | .LSOverride 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear on external disk 40 | .Spotlight-V100 41 | .Trashes 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | /nbproject 50 | -------------------------------------------------------------------------------- /tests/pFFA-EnumData-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | $this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']); 17 | } 18 | 19 | public function testEnumData_manufacturer() 20 | { 21 | $this->assertEquals('Garmin', $this->pFFA->manufacturer()); 22 | } 23 | 24 | public function testEnumData_product() 25 | { 26 | $this->assertEquals('Forerunner 910XT', $this->pFFA->product()); 27 | } 28 | 29 | public function testEnumData_sport() 30 | { 31 | $this->assertEquals('Swimming', $this->pFFA->sport()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Adrian Gibbons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/pFFA-IsPaused-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | $this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']); 17 | } 18 | 19 | public function testIsPaused() 20 | { 21 | // isPaused() returns array of booleans using timestamp as key. 22 | $is_paused = $this->pFFA->isPaused(); 23 | 24 | // Assert number of timestamps 25 | $this->assertEquals(3190, count($is_paused)); 26 | 27 | // Assert an arbitrary element/timestamps is true 28 | $this->assertEquals(true, $is_paused[1437477706]); 29 | 30 | // Assert an arbitrary element/timestamps is false 31 | $this->assertEquals(false, $is_paused[1437474517]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/pFFA-GetJSON-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | $this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']); 17 | } 18 | 19 | public function testGetJSON() 20 | { 21 | // getJSON() create a JSON object that contains available record message information. 22 | $crank_length = null; 23 | $ftp = null; 24 | $data_required = ['timestamp', 'speed']; 25 | $selected_cadence = 90; 26 | $php_object = json_decode($this->pFFA->getJSON($crank_length, $ftp, $data_required, $selected_cadence)); 27 | 28 | // Assert data 29 | $this->assertEquals('raw', $php_object->units); 30 | $this->assertEquals(3043, count($php_object->data)); 31 | $this->assertEquals(1437474517, $php_object->data[0]->timestamp); 32 | $this->assertEquals(1.378, $php_object->data[0]->speed); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/pFFA-HR-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | $this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']); 17 | } 18 | 19 | public function testHR_hrMetrics() 20 | { 21 | $hr_metrics = $this->pFFA->hrMetrics(50, 190, 170, 'male'); 22 | 23 | $this->assertEquals(74, $hr_metrics['TRIMPexp']); 24 | $this->assertEquals(0.8, $hr_metrics['hrIF']); 25 | } 26 | 27 | public function testHR_hrPartionedHRmaximum() 28 | { 29 | // Calls phpFITFileAnalysis::hrZonesMax() 30 | $hr_partioned_HRmaximum = $this->pFFA->hrPartionedHRmaximum(190); 31 | 32 | $this->assertEquals(19.4, $hr_partioned_HRmaximum['0-113']); 33 | $this->assertEquals(33.1, $hr_partioned_HRmaximum['114-142']); 34 | $this->assertEquals(31.4, $hr_partioned_HRmaximum['143-161']); 35 | $this->assertEquals(16.1, $hr_partioned_HRmaximum['162-180']); 36 | $this->assertEquals(0, $hr_partioned_HRmaximum['181+']); 37 | } 38 | 39 | public function testHR_hrPartionedHRreserve() 40 | { 41 | // Calls phpFITFileAnalysis::hrZonesReserve() 42 | $hr_partioned_HRreserve = $this->pFFA->hrPartionedHRreserve(50, 190); 43 | 44 | $this->assertEquals(45.1, $hr_partioned_HRreserve['0-133']); 45 | $this->assertEquals(5.8, $hr_partioned_HRreserve['134-140']); 46 | $this->assertEquals(20.1, $hr_partioned_HRreserve['141-154']); 47 | $this->assertEquals(15.9, $hr_partioned_HRreserve['155-164']); 48 | $this->assertEquals(12.5, $hr_partioned_HRreserve['165-174']); 49 | $this->assertEquals(0.6, $hr_partioned_HRreserve['175-181']); 50 | $this->assertEquals(0, $hr_partioned_HRreserve['182+']); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/pFFA-QuadrantAnalysis-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | $this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']); 17 | } 18 | 19 | public function testQuadrantAnalysis() 20 | { 21 | $crank_length = 0.175; 22 | $ftp = 329; 23 | $selected_cadence = 90; 24 | $use_timestamps = false; 25 | 26 | // quadrantAnalysis() returns an array that can be used to plot CPV vs AEPF. 27 | $quadrant_plot = $this->pFFA->quadrantAnalysis($crank_length, $ftp, $selected_cadence, $use_timestamps); 28 | 29 | $this->assertEquals(90, $quadrant_plot['selected_cadence']); 30 | 31 | $this->assertEquals(199.474, $quadrant_plot['aepf_threshold']); 32 | $this->assertEquals(1.649, $quadrant_plot['cpv_threshold']); 33 | 34 | $this->assertEquals(10.48, $quadrant_plot['quad_percent']['hf_hv']); 35 | $this->assertEquals(10.61, $quadrant_plot['quad_percent']['hf_lv']); 36 | $this->assertEquals(14.00, $quadrant_plot['quad_percent']['lf_hv']); 37 | $this->assertEquals(64.91, $quadrant_plot['quad_percent']['lf_lv']); 38 | 39 | $this->assertEquals(1.118, $quadrant_plot['plot'][0][0]); 40 | $this->assertEquals(47.411, $quadrant_plot['plot'][0][1]); 41 | 42 | $this->assertEquals(0.367, $quadrant_plot['ftp-25w'][0][0]); 43 | $this->assertEquals(829.425, $quadrant_plot['ftp-25w'][0][1]); 44 | 45 | $this->assertEquals(0.367, $quadrant_plot['ftp'][0][0]); 46 | $this->assertEquals(897.634, $quadrant_plot['ftp'][0][1]); 47 | 48 | $this->assertEquals(0.367, $quadrant_plot['ftp+25w'][0][0]); 49 | $this->assertEquals(965.843, $quadrant_plot['ftp+25w'][0][1]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/pFFA-SetUnits-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 15 | } 16 | 17 | public function testSetUnits_validate_options_pass() 18 | { 19 | $valid_options = ['raw', 'statute', 'metric']; 20 | foreach($valid_options as $valid_option) { 21 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => $valid_option]); 22 | 23 | if($valid_option === 'raw') { 24 | $this->assertEquals(1.286, reset($pFFA->data_mesgs['record']['speed'])); 25 | } 26 | if($valid_option === 'statute') { 27 | $this->assertEquals(2.877, reset($pFFA->data_mesgs['record']['speed'])); 28 | } 29 | if($valid_option === 'metric') { 30 | $this->assertEquals(4.63, reset($pFFA->data_mesgs['record']['speed'])); 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * @expectedException Exception 37 | */ 38 | public function testSetUnits_validate_options_fail() 39 | { 40 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'INVALID']); 41 | } 42 | 43 | public function testSetUnits_validate_pace_option_pass() 44 | { 45 | $valid_options = [true, false]; 46 | foreach($valid_options as $valid_option) { 47 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw', 'pace' => $valid_option]); 48 | 49 | $this->assertEquals(1.286, reset($pFFA->data_mesgs['record']['speed'])); 50 | } 51 | } 52 | 53 | /** 54 | * @expectedException Exception 55 | */ 56 | public function testSetUnits_validate_pace_option_fail() 57 | { 58 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 'INVALID']); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | phpFITFileAnalysis demo 6 | 7 | 8 | 9 | 10 |
11 |
12 |

phpFITFileAnalysis A PHP class for analysing FIT files created by Garmin GPS devices.

13 |

This is a demonstration of the phpFITFileAnalysis class available on GitHub

14 |
15 |
16 |
17 |
18 |
19 |
20 |

Mountain Biking

21 |
22 |
23 | Mountain Biking 24 |
25 |
26 |
27 |
28 |
29 |
30 |

Mountain Biking (Leaflet maps)

31 |
32 |
33 | Mountain Biking 34 |
35 |
36 |
37 |
38 |
39 |
40 |

Power Analysis (cycling)

41 |
42 |
43 | Power Analysis 44 |
45 |
46 |
47 |
48 |
49 |
50 |

Quadrant Analysis

51 |
52 |
53 | Quadrant Analysis 54 |
55 |
56 |
57 |
58 |
59 |
60 |

Swim

61 |
62 |
63 | Swim 64 |
65 |
66 |
67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /demo/libraries/PolylineEncoder.php: -------------------------------------------------------------------------------- 1 | points = array(); 15 | } 16 | 17 | /** 18 | * Add a point 19 | * 20 | * @param float $lat : lattitude 21 | * @param float $lng : longitude 22 | */ 23 | function addPoint($lat, $lng) { 24 | if (empty($this->points)) { 25 | $this->points[] = array('x' => $lat, 'y' => $lng); 26 | $this->encoded = $this->encodeValue($lat) . $this->encodeValue($lng); 27 | } else { 28 | $n = count($this->points); 29 | $prev_p = $this->points[$n-1]; 30 | $this->points[] = array('x' => $lat, 'y' => $lng); 31 | $this->encoded .= $this->encodeValue($lat-$prev_p['x']) . $this->encodeValue($lng-$prev_p['y']); 32 | } 33 | } 34 | 35 | /** 36 | * Return the encoded string generated from the points 37 | * 38 | * @return string 39 | */ 40 | function encodedString() { 41 | return $this->encoded; 42 | } 43 | 44 | /** 45 | * Encode a value following Google Maps API v3 algorithm 46 | * 47 | * @param type $value 48 | * @return type 49 | */ 50 | function encodeValue($value) { 51 | $encoded = ""; 52 | $value = round($value * 100000); 53 | $r = ($value < 0) ? ~($value << 1) : ($value << 1); 54 | 55 | while ($r >= 0x20) { 56 | $val = (0x20|($r & 0x1f)) + 63; 57 | $encoded .= chr($val); 58 | $r >>= 5; 59 | } 60 | $lastVal = $r + 63; 61 | $encoded .= chr($lastVal); 62 | return $encoded; 63 | } 64 | 65 | /** 66 | * Decode an encoded polyline string to an array of points 67 | * 68 | * @param type $value 69 | * @return type 70 | */ 71 | static public function decodeValue($value) { 72 | $index = 0; 73 | $points = array(); 74 | $lat = 0; 75 | $lng = 0; 76 | 77 | while ($index < strlen($value)) { 78 | $b; 79 | $shift = 0; 80 | $result = 0; 81 | do { 82 | $b = ord(substr($value, $index++, 1)) - 63; 83 | $result |= ($b & 0x1f) << $shift; 84 | $shift += 5; 85 | } while ($b > 31); 86 | $dlat = (($result & 1) ? ~($result >> 1) : ($result >> 1)); 87 | $lat += $dlat; 88 | 89 | $shift = 0; 90 | $result = 0; 91 | do { 92 | $b = ord(substr($value, $index++, 1)) - 63; 93 | $result |= ($b & 0x1f) << $shift; 94 | $shift += 5; 95 | } while ($b > 31); 96 | $dlng = (($result & 1) ? ~($result >> 1) : ($result >> 1)); 97 | $lng += $dlng; 98 | 99 | $points[] = array('x' => $lat/100000, 'y' => $lng/100000); 100 | } 101 | 102 | return $points; 103 | } 104 | } -------------------------------------------------------------------------------- /tests/pFFA-Basic-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | } 17 | 18 | public function testDemoFilesExist() 19 | { 20 | $this->demo_files = array_values(array_diff(scandir($this->base_dir), array('..', '.'))); 21 | sort($this->demo_files); 22 | sort($this->valid_files); 23 | $this->assertEquals($this->valid_files, $this->demo_files); 24 | var_dump($this->demo_files); 25 | } 26 | 27 | /** 28 | * @expectedException Exception 29 | */ 30 | public function testEmptyFilepath() 31 | { 32 | $pFFA = new adriangibbons\phpFITFileAnalysis(''); 33 | } 34 | 35 | /** 36 | * @expectedException Exception 37 | */ 38 | public function testFileDoesntExist() 39 | { 40 | $pFFA = new adriangibbons\phpFITFileAnalysis('file_doesnt_exist.fit'); 41 | } 42 | 43 | /** 44 | * @expectedException Exception 45 | */ 46 | public function testInvalidFitFile() 47 | { 48 | $file_path = $this->base_dir . '../composer.json'; 49 | $pFFA = new adriangibbons\phpFITFileAnalysis($file_path); 50 | } 51 | 52 | 53 | public function testDemoFileBasics() 54 | { 55 | foreach($this->demo_files as $filename) { 56 | 57 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $filename); 58 | 59 | $this->assertGreaterThan(0, $pFFA->data_mesgs['activity']['timestamp'], 'No Activity timestamp!'); 60 | 61 | if (isset($pFFA->data_mesgs['record'])) { 62 | $this->assertGreaterThan(0, count($pFFA->data_mesgs['record']['timestamp']), 'No Record timestamps!'); 63 | 64 | // Check if distance from record messages is +/- 2% of distance from session message 65 | if (is_array($pFFA->data_mesgs['record']['distance'])) { 66 | $distance_difference = abs(end($pFFA->data_mesgs['record']['distance']) - $pFFA->data_mesgs['session']['total_distance'] / 1000); 67 | $this->assertLessThan(0.02 * end($pFFA->data_mesgs['record']['distance']), $distance_difference, 'Session distance should be similar to last Record distance'); 68 | } 69 | 70 | // Look for big jumps in latitude and longitude 71 | if (isset($pFFA->data_mesgs['record']['position_lat']) && is_array($pFFA->data_mesgs['record']['position_lat'])) { 72 | foreach ($pFFA->data_mesgs['record']['position_lat'] as $key => $value) { 73 | if (isset($pFFA->data_mesgs['record']['position_lat'][$key - 1])) { 74 | if (abs($pFFA->data_mesgs['record']['position_lat'][$key - 1] - $pFFA->data_mesgs['record']['position_lat'][$key]) > 1) { 75 | $this->assertTrue(false, 'Too big a jump in latitude'); 76 | } 77 | } 78 | } 79 | } 80 | if (isset($pFFA->data_mesgs['record']['position_long']) && is_array($pFFA->data_mesgs['record']['position_long'])) { 81 | foreach ($pFFA->data_mesgs['record']['position_long'] as $key => $value) { 82 | if (isset($pFFA->data_mesgs['record']['position_long'][$key - 1])) { 83 | if (abs($pFFA->data_mesgs['record']['position_long'][$key - 1] - $pFFA->data_mesgs['record']['position_long'][$key]) > 1) { 84 | $this->assertTrue(false, 'Too big a jump in longitude'); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /demo/libraries/Line_DouglasPeucker.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * My invaluable references: 22 | * https://github.com/Polzme/geoPHP/commit/56c9072f69ed1cec2fdd36da76fa595792b4aa24 23 | * http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm 24 | * http://math.ucsd.edu/~wgarner/math4c/derivations/distance/distptline.htm 25 | */ 26 | 27 | function simplify_RDP($vertices, $tolerance) { 28 | // if this is a multilinestring, then we call ourselves one each segment individually, collect the list, and return that list of simplified lists 29 | if (is_array($vertices[0][0])) { 30 | $multi = array(); 31 | foreach ($vertices as $subvertices) $multi[] = simplify_RDP($subvertices,$tolerance); 32 | return $multi; 33 | } 34 | 35 | $tolerance2 = $tolerance * $tolerance; 36 | 37 | // okay, so this is a single linestring and we simplify it individually 38 | return _segment_RDP($vertices,$tolerance2); 39 | } 40 | 41 | function _segment_RDP($segment, $tolerance_squared) { 42 | if (sizeof($segment) <= 2) return $segment; // segment is too small to simplify, hand it back as-is 43 | 44 | // find the maximum distance (squared) between this line $segment and each vertex 45 | // distance is solved as described at UCSD page linked above 46 | // cheat: vertical lines (directly north-south) have no slope so we fudge it with a very tiny nudge to one vertex; can't imagine any units where this will matter 47 | $startx = (float) $segment[0][0]; 48 | $starty = (float) $segment[0][1]; 49 | $endx = (float) $segment[ sizeof($segment)-1 ][0]; 50 | $endy = (float) $segment[ sizeof($segment)-1 ][1]; 51 | if ($endx == $startx) $startx += 0.00001; 52 | $m = ($endy - $starty) / ($endx - $startx); // slope, as in y = mx + b 53 | $b = $starty - ($m * $startx); // y-intercept, as in y = mx + b 54 | 55 | $max_distance_squared = 0; 56 | $max_distance_index = null; 57 | for ($i=1, $l=sizeof($segment); $i<=$l-2; $i++) { 58 | $x1 = $segment[$i][0]; 59 | $y1 = $segment[$i][1]; 60 | 61 | $closestx = ( ($m*$y1) + ($x1) - ($m*$b) ) / ( ($m*$m)+1); 62 | $closesty = ($m * $closestx) + $b; 63 | $distsqr = ($closestx-$x1)*($closestx-$x1) + ($closesty-$y1)*($closesty-$y1); 64 | 65 | if ($distsqr > $max_distance_squared) { 66 | $max_distance_squared = $distsqr; 67 | $max_distance_index = $i; 68 | } 69 | } 70 | 71 | // cleanup and disposition 72 | // if the max distance is below tolerance, we can bail, giving a straight line between the start vertex and end vertex (all points are so close to the straight line) 73 | if ($max_distance_squared <= $tolerance_squared) { 74 | return array($segment[0], $segment[ sizeof($segment)-1 ]); 75 | } 76 | // but if we got here then a vertex falls outside the tolerance 77 | // split the line segment into two smaller segments at that "maximum error vertex" and simplify those 78 | $slice1 = array_slice($segment, 0, $max_distance_index); 79 | $slice2 = array_slice($segment, $max_distance_index); 80 | $segs1 = _segment_RDP($slice1, $tolerance_squared); 81 | $segs2 = _segment_RDP($slice2, $tolerance_squared); 82 | return array_merge($segs1,$segs2); 83 | } -------------------------------------------------------------------------------- /tests/pFFA-FixData-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | } 17 | 18 | /** 19 | * Original road-cycling.fit before fixData() contains: 20 | * 21 | * record message | count() 22 | * -----------------+-------- 23 | * timestamp | 4317 24 | * position_lat | 4309 <- test 25 | * position_long | 4309 <- test 26 | * distance | 4309 <- test 27 | * altitude | 4317 28 | * speed | 4309 <- test 29 | * heart_rate | 4316 <- test 30 | * temperature | 4317 31 | */ 32 | public function testFixData_before() 33 | { 34 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename); 35 | 36 | $this->assertEquals(4309, count($pFFA->data_mesgs['record']['position_lat'])); 37 | $this->assertEquals(4309, count($pFFA->data_mesgs['record']['position_long'])); 38 | $this->assertEquals(4309, count($pFFA->data_mesgs['record']['distance'])); 39 | $this->assertEquals(4309, count($pFFA->data_mesgs['record']['speed'])); 40 | $this->assertEquals(4316, count($pFFA->data_mesgs['record']['heart_rate'])); 41 | 42 | $pFFA2 = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename2); 43 | $this->assertEquals(3043, count($pFFA2->data_mesgs['record']['cadence'])); 44 | $this->assertEquals(3043, count($pFFA2->data_mesgs['record']['power'])); 45 | } 46 | 47 | /** 48 | * $pFFA->data_mesgs['record']['heart_rate'] 49 | * [805987191 => 118], 50 | * [805987192 => missing], 51 | * [805987193 => 117] 52 | */ 53 | public function testFixData_hr_missing_key() 54 | { 55 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename); 56 | 57 | $hr_missing_key = array_diff($pFFA->data_mesgs['record']['timestamp'], array_keys($pFFA->data_mesgs['record']['heart_rate'])); 58 | $this->assertEquals([3036 => 1437052792], $hr_missing_key); 59 | } 60 | 61 | public function testFixData_after() 62 | { 63 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['all']]); 64 | $this->assertEquals(4317, count($pFFA->data_mesgs['record']['position_lat'])); 65 | $this->assertEquals(4317, count($pFFA->data_mesgs['record']['position_long'])); 66 | $this->assertEquals(4317, count($pFFA->data_mesgs['record']['distance'])); 67 | $this->assertEquals(4317, count($pFFA->data_mesgs['record']['speed'])); 68 | $this->assertEquals(4317, count($pFFA->data_mesgs['record']['heart_rate'])); 69 | 70 | $pFFA2 = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename2, ['fix_data' => ['cadence', 'power']]); 71 | $this->assertEquals(3043, count($pFFA2->data_mesgs['record']['cadence'])); 72 | $this->assertEquals(3043, count($pFFA2->data_mesgs['record']['power'])); 73 | } 74 | 75 | /** 76 | * $pFFA->data_mesgs['record']['heart_rate'] 77 | * [805987191 => 118], 78 | * [805987192 => 117.5], 79 | * [805987193 => 117] 80 | */ 81 | public function testFixData_hr_missing_key_fixed() 82 | { 83 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['heart_rate']]); 84 | 85 | $this->assertEquals(117.5, $pFFA->data_mesgs['record']['heart_rate'][1437052792]); 86 | } 87 | 88 | public function testFixData_validate_options_pass() 89 | { 90 | // Positive testing 91 | $valid_options = ['all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'speed', 'power']; 92 | foreach($valid_options as $valid_option) { 93 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => [$valid_option]]); 94 | } 95 | } 96 | 97 | public function testFixData_data_every_second() 98 | { 99 | $options = [ 100 | 'fix_data' => ['speed'], 101 | 'data_every_second' => true, 102 | 'units' => 'raw', 103 | ]; 104 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, $options); 105 | 106 | $this->assertEquals(6847, count($pFFA->data_mesgs['record']['speed'])); 107 | } 108 | 109 | /** 110 | * @expectedException Exception 111 | */ 112 | public function testFixData_validate_options_fail() 113 | { 114 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['INVALID']]); 115 | } 116 | 117 | /** 118 | * @expectedException Exception 119 | */ 120 | public function testFixData_invalid_pace_option() 121 | { 122 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 'INVALID']); 123 | } 124 | 125 | /** 126 | * @expectedException Exception 127 | */ 128 | public function testFixData_invalid_pace_option2() 129 | { 130 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 123456]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/pFFA-Power-Test.php: -------------------------------------------------------------------------------- 1 | base_dir = __DIR__ . '/../demo/fit_files/'; 16 | $this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']); 17 | } 18 | 19 | public function testPower_criticalPower_values() 20 | { 21 | $time_periods = [2,5,10,60,300,600,1200,1800,3600]; 22 | $cps = $this->pFFA->criticalPower($time_periods); 23 | 24 | array_walk($cps, function(&$v) { $v = round($v, 2); }); 25 | 26 | $this->assertEquals(551.50, $cps[2]); 27 | $this->assertEquals(542.20, $cps[5]); 28 | $this->assertEquals(527.70, $cps[10]); 29 | $this->assertEquals(452.87, $cps[60]); 30 | $this->assertEquals(361.99, $cps[300]); 31 | $this->assertEquals(328.86, $cps[600]); 32 | $this->assertEquals(260.52, $cps[1200]); 33 | $this->assertEquals(221.81, $cps[1800]); 34 | } 35 | 36 | public function testPower_criticalPower_time_period_max() 37 | { 38 | // 14400 seconds is 4 hours and longer than file duration so should only get one result back (for 2 seconds) 39 | $time_periods = [2,14400]; 40 | $cps = $this->pFFA->criticalPower($time_periods); 41 | 42 | $this->assertEquals(1, count($cps)); 43 | } 44 | 45 | public function testPower_powerMetrics() 46 | { 47 | $power_metrics = $this->pFFA->powerMetrics(350); 48 | 49 | $this->assertEquals(221, $power_metrics['Average Power']); 50 | $this->assertEquals(671, $power_metrics['Kilojoules']); 51 | $this->assertEquals(285, $power_metrics['Normalised Power']); 52 | $this->assertEquals(1.29, $power_metrics['Variability Index']); 53 | $this->assertEquals(0.81, $power_metrics['Intensity Factor']); 54 | $this->assertEquals(56, $power_metrics['Training Stress Score']); 55 | } 56 | 57 | public function testPower_power_partitioned() 58 | { 59 | // Calls phpFITFileAnalysis::powerZones(); 60 | $power_partioned = $this->pFFA->powerPartioned(350); 61 | 62 | $this->assertEquals(45.2, $power_partioned['0-193']); 63 | $this->assertEquals(10.8, $power_partioned['194-263']); 64 | $this->assertEquals(18.1, $power_partioned['264-315']); 65 | $this->assertEquals(17.9, $power_partioned['316-368']); 66 | $this->assertEquals(4.2, $power_partioned['369-420']); 67 | $this->assertEquals(3.3, $power_partioned['421-525']); 68 | $this->assertEquals(0.4, $power_partioned['526+']); 69 | } 70 | 71 | public function testPower_powerHistogram() 72 | { 73 | // Calls phpFITFileAnalysis::histogram(); 74 | $power_histogram = $this->pFFA->powerHistogram(100); 75 | 76 | $this->assertEquals(374, $power_histogram[0]); 77 | $this->assertEquals(634, $power_histogram[100]); 78 | $this->assertEquals(561, $power_histogram[200]); 79 | $this->assertEquals(1103, $power_histogram[300]); 80 | $this->assertEquals(301, $power_histogram[400]); 81 | $this->assertEquals(66, $power_histogram[500]); 82 | $this->assertEquals(4, $power_histogram[600]); 83 | } 84 | 85 | /** 86 | * @expectedException Exception 87 | */ 88 | public function testPower_criticalPower_no_power() 89 | { 90 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit'); 91 | 92 | $time_periods = [2,14400]; 93 | $cps = $pFFA->criticalPower($time_periods); 94 | } 95 | 96 | /** 97 | * @expectedException Exception 98 | */ 99 | public function testPower_powerMetrics_no_power() 100 | { 101 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit'); 102 | 103 | $power_metrics = $pFFA->powerMetrics(350); 104 | } 105 | 106 | /** 107 | * @expectedException Exception 108 | */ 109 | public function testPower_powerHistogram_no_power() 110 | { 111 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit'); 112 | 113 | $power_metrics = $pFFA->powerHistogram(100); 114 | } 115 | 116 | /** 117 | * @expectedException Exception 118 | */ 119 | public function testPower_powerHistogram_invalid_bucket_width() 120 | { 121 | $power_histogram = $this->pFFA->powerHistogram('INVALID'); 122 | } 123 | 124 | /** 125 | * @expectedException Exception 126 | */ 127 | public function testPower_power_partitioned_no_power() 128 | { 129 | $pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit'); 130 | 131 | $power_partioned = $pFFA->powerPartioned(350); 132 | } 133 | 134 | /** 135 | * @expectedException Exception 136 | */ 137 | public function testPower_power_partitioned_not_array() 138 | { 139 | $power_histogram = $this->pFFA->partitionData('power', 123456); 140 | } 141 | 142 | /** 143 | * @expectedException Exception 144 | */ 145 | public function testPower_power_partitioned_not_numeric() 146 | { 147 | $power_histogram = $this->pFFA->partitionData('power', [200, 400, 'INVALID']); 148 | } 149 | 150 | /** 151 | * @expectedException Exception 152 | */ 153 | public function testPower_power_partitioned_not_ascending() 154 | { 155 | $power_histogram = $this->pFFA->partitionData('power', [400, 200]); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /demo/css/dc.css: -------------------------------------------------------------------------------- 1 | #lap-row-chart svg g g.axis { display: none; } 2 | 3 | div.dc-chart { 4 | float: left; 5 | } 6 | 7 | .dc-chart rect.bar { 8 | stroke: none; 9 | cursor: pointer; 10 | } 11 | 12 | .dc-chart rect.bar:hover { 13 | fill-opacity: .5; 14 | } 15 | 16 | .dc-chart rect.stack1 { 17 | stroke: none; 18 | fill: red; 19 | } 20 | 21 | .dc-chart rect.stack2 { 22 | stroke: none; 23 | fill: green; 24 | } 25 | 26 | .dc-chart rect.deselected { 27 | stroke: none; 28 | fill: #ccc; 29 | } 30 | 31 | .dc-chart .empty-chart .pie-slice path { 32 | fill: #FFEEEE; 33 | cursor: default; 34 | } 35 | 36 | .dc-chart .empty-chart .pie-slice { 37 | cursor: default; 38 | } 39 | 40 | .dc-chart .pie-slice { 41 | fill: white; 42 | font-size: 12px; 43 | cursor: pointer; 44 | } 45 | 46 | .dc-chart .pie-slice.external{ 47 | fill: black; 48 | } 49 | 50 | .dc-chart .pie-slice :hover { 51 | fill-opacity: .8; 52 | } 53 | 54 | .dc-chart .pie-slice.highlight { 55 | fill-opacity: .8; 56 | } 57 | 58 | .dc-chart .selected path { 59 | stroke-width: 3; 60 | stroke: #ccc; 61 | fill-opacity: 1; 62 | } 63 | 64 | .dc-chart .deselected path { 65 | stroke: none; 66 | fill-opacity: .5; 67 | fill: #ccc; 68 | } 69 | 70 | .dc-chart .axis path, .axis line { 71 | fill: none; 72 | stroke: #000; 73 | shape-rendering: crispEdges; 74 | } 75 | 76 | .dc-chart .axis text { 77 | font: 10px sans-serif; 78 | } 79 | 80 | .dc-chart .grid-line { 81 | fill: none; 82 | stroke: #ccc; 83 | opacity: .5; 84 | shape-rendering: crispEdges; 85 | } 86 | 87 | .dc-chart .grid-line line { 88 | fill: none; 89 | stroke: #ccc; 90 | opacity: .5; 91 | shape-rendering: crispEdges; 92 | } 93 | 94 | .dc-chart .brush rect.background { 95 | z-index: -999; 96 | } 97 | 98 | .dc-chart .brush rect.extent { 99 | fill: steelblue; 100 | fill-opacity: .125; 101 | } 102 | 103 | .dc-chart .brush .resize path { 104 | fill: #eee; 105 | stroke: #666; 106 | } 107 | 108 | .dc-chart path.line { 109 | fill: none; 110 | stroke-width: 1.5px; 111 | } 112 | 113 | .dc-chart circle.dot { 114 | stroke: none; 115 | } 116 | 117 | .dc-chart g.dc-tooltip path { 118 | fill: none; 119 | stroke: grey; 120 | stroke-opacity: .8; 121 | } 122 | 123 | .dc-chart path.area { 124 | fill-opacity: .3; 125 | stroke: none; 126 | } 127 | 128 | .dc-chart .node { 129 | font-size: 0.7em; 130 | cursor: pointer; 131 | } 132 | 133 | .dc-chart .node :hover { 134 | fill-opacity: .8; 135 | } 136 | 137 | .dc-chart .selected circle { 138 | stroke-width: 3; 139 | stroke: #ccc; 140 | fill-opacity: 1; 141 | } 142 | 143 | .dc-chart .deselected circle { 144 | stroke: none; 145 | fill-opacity: .5; 146 | fill: #ccc; 147 | } 148 | 149 | .dc-chart .bubble { 150 | stroke: none; 151 | fill-opacity: 0.6; 152 | } 153 | 154 | .dc-data-count { 155 | float: right; 156 | margin-top: 15px; 157 | margin-right: 15px; 158 | } 159 | 160 | .dc-data-count .filter-count { 161 | color: #3182bd; 162 | font-weight: bold; 163 | } 164 | 165 | .dc-data-count .total-count { 166 | color: #3182bd; 167 | font-weight: bold; 168 | } 169 | 170 | .dc-data-table { 171 | } 172 | 173 | .dc-chart g.state { 174 | cursor: pointer; 175 | } 176 | 177 | .dc-chart g.state :hover { 178 | fill-opacity: .8; 179 | } 180 | 181 | .dc-chart g.state path { 182 | stroke: white; 183 | } 184 | 185 | .dc-chart g.selected path { 186 | } 187 | 188 | .dc-chart g.deselected path { 189 | fill: grey; 190 | } 191 | 192 | .dc-chart g.selected text { 193 | } 194 | 195 | .dc-chart g.deselected text { 196 | display: none; 197 | } 198 | 199 | .dc-chart g.county path { 200 | stroke: white; 201 | fill: none; 202 | } 203 | 204 | .dc-chart g.debug rect { 205 | fill: blue; 206 | fill-opacity: .2; 207 | } 208 | 209 | .dc-chart g.row rect { 210 | fill-opacity: 0.8; 211 | cursor: pointer; 212 | } 213 | 214 | .dc-chart g.row rect:hover { 215 | fill-opacity: 0.6; 216 | } 217 | 218 | .dc-chart g.row text { 219 | fill: #333; 220 | font-size: 12px; 221 | cursor: pointer; 222 | } 223 | 224 | .dc-legend { 225 | font-size: 11px; 226 | } 227 | 228 | .dc-legend-item { 229 | cursor: pointer; 230 | } 231 | 232 | .dc-chart g.axis text { 233 | /* Makes it so the user can't accidentally click and select text that is meant as a label only */ 234 | -webkit-user-select: none; /* Chrome/Safari */ 235 | -moz-user-select: none; /* Firefox */ 236 | -ms-user-select: none; /* IE10 */ 237 | -o-user-select: none; 238 | user-select: none; 239 | pointer-events: none; 240 | } 241 | 242 | .dc-chart path.highlight { 243 | stroke-width: 3; 244 | fill-opacity: 1; 245 | stroke-opacity: 1; 246 | } 247 | 248 | .dc-chart .highlight { 249 | fill-opacity: 1; 250 | stroke-opacity: 1; 251 | } 252 | 253 | .dc-chart .fadeout { 254 | fill-opacity: 0.2; 255 | stroke-opacity: 0.2; 256 | } 257 | 258 | .dc-chart path.dc-symbol, g.dc-legend-item.fadeout { 259 | fill-opacity: 0.5; 260 | stroke-opacity: 0.5; 261 | } 262 | 263 | .dc-hard .number-display { 264 | float: none; 265 | } 266 | 267 | .dc-chart .box text { 268 | font: 10px sans-serif; 269 | -webkit-user-select: none; /* Chrome/Safari */ 270 | -moz-user-select: none; /* Firefox */ 271 | -ms-user-select: none; /* IE10 */ 272 | -o-user-select: none; 273 | user-select: none; 274 | pointer-events: none; 275 | } 276 | 277 | .dc-chart .box line, 278 | .dc-chart .box circle { 279 | fill: #fff; 280 | stroke: #000; 281 | stroke-width: 1.5px; 282 | } 283 | 284 | .dc-chart .box rect { 285 | stroke: #000; 286 | stroke-width: 1.5px; 287 | } 288 | 289 | .dc-chart .box .center { 290 | stroke-dasharray: 3,3; 291 | } 292 | 293 | .dc-chart .box .outlier { 294 | fill: none; 295 | stroke: #ccc; 296 | } 297 | 298 | .dc-chart .box.deselected .box { 299 | fill: #ccc; 300 | } 301 | 302 | .dc-chart .box.deselected { 303 | opacity: .5; 304 | } 305 | 306 | .dc-chart .symbol{ 307 | stroke: none; 308 | } 309 | 310 | .dc-chart .heatmap .box-group.deselected rect { 311 | stroke: none; 312 | fill-opacity: .5; 313 | fill: #ccc; 314 | } 315 | 316 | .dc-chart .heatmap g.axis text { 317 | pointer-events: all; 318 | cursor: pointer; 319 | } 320 | -------------------------------------------------------------------------------- /demo/swim.php: -------------------------------------------------------------------------------- 1 | [], 14 | 'units' => 'raw', 15 | // 'pace' => false 16 | ]; 17 | $pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options); 18 | } catch (Exception $e) { 19 | echo 'caught exception: '.$e->getMessage(); 20 | die(); 21 | } 22 | $units = 'm'; 23 | $pool_length = $pFFA->data_mesgs['session']['pool_length']; 24 | $total_distance = number_format($pFFA->data_mesgs['record']['distance']); 25 | if ($pFFA->enumData('display_measure', $pFFA->data_mesgs['session']['pool_length_unit']) == 'statute') { 26 | $pool_length = round($pFFA->data_mesgs['session']['pool_length'] * 1.0936133); 27 | $total_distance = number_format($pFFA->data_mesgs['record']['distance'] * 1.0936133); 28 | $units = 'yd'; 29 | } 30 | ?> 31 | 32 | 33 | 34 | 35 | phpFITFileAnalysis demo 36 | 37 | 38 | 39 | 40 |
41 |
42 |

phpFITFileAnalysis A PHP class for analysing FIT files created by Garmin GPS devices.

43 |

This is a demonstration of the phpFITFileAnalysis class available on GitHub

44 |
45 |
46 |
47 |
48 |
49 |
50 |

FIT File info

51 |
52 |
53 |
54 |
File:
55 |
56 |
Device:
57 |
manufacturer() . ' ' . $pFFA->product(); ?>
58 |
Sport:
59 |
sport(); ?>
60 |
Pool length:
61 |
62 |
Duration:
63 |
data_mesgs['session']['total_elapsed_time']); ?>
64 |
Total distance:
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Lap Time vs. Number of Strokes

72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |

Length Message fields

82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | data_mesgs['length']['total_timer_time']); 94 | $active_length = 0; 95 | for ($i=0; $i<$lengths; $i++) { 96 | $min = floor($pFFA->data_mesgs['length']['total_timer_time'][$i] / 60); 97 | $sec = number_format($pFFA->data_mesgs['length']['total_timer_time'][$i] - ($min*60), 1); 98 | $dur = $min.':'.$sec; 99 | if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') { 100 | echo ''; 101 | echo ''; 102 | echo ''; 103 | echo ''; 104 | echo ''; 105 | echo ''; 106 | echo ''; 107 | $active_length++; 108 | } else { 109 | echo ''; 110 | echo ''; 111 | echo ''; 112 | echo ''; 113 | echo ''; 114 | echo ''; 115 | } 116 | } 117 | ?> 118 | 119 |
LengthTime (min:sec)# StrokesStroke
'.($i+1).''.$dur.''.$pFFA->data_mesgs['length']['total_strokes'][$i].''.$pFFA->enumData('swim_stroke', $pFFA->data_mesgs['length']['swim_stroke'][$active_length]).'
'.($i+1).''.$dur.'-Rest
120 |
121 |
122 |
123 |
124 | 125 | 126 | 127 | 184 | 185 | -------------------------------------------------------------------------------- /utils/readProfileCSV.php: -------------------------------------------------------------------------------- 1 | [\n\t\t'type' => '$data[1]',\n"; 25 | $enum_first = false; 26 | } else { 27 | $enum .= "\t],\n\t'$data[0]' => [ \n\t\t'type' => '$data[1]',\n"; 28 | } 29 | } else { 30 | $enum .= "\t\t$data[3] => '$data[2]', " . (!empty($data[4]) ? "// $data[4]\n" : "\n"); 31 | } 32 | } 33 | 34 | $enum .= "\n\t]\n];\n"; 35 | 36 | fclose($open); 37 | } else { 38 | echo "The CSV version of the Profiles.xlsx Types tab needs to be available in the local directory.\n"; 39 | exit(1); 40 | } 41 | $fp = fopen('type-enums.php', 'w'); 42 | fwrite($fp, " [ // $data[13]\n"; 68 | $data_msg .= array_search($data[0], $enum_data['mesg_num']) . " => ['mesg_name' =>'$data[0]', 'field_defns' => [\n\t"; 69 | $data_first = false; 70 | } else { 71 | $data_msg .= "]],\n\t " . array_search($data[0], $enum_data['mesg_num']) . " => ['mesg_name' =>'$data[0]', 'field_defns' => [\n"; 72 | } 73 | } else { 74 | if (!is_numeric($data[1])) { 75 | // Start Dynamic section, remove previous closure 76 | $data_msg = addDynamic($open, $data, $data_msg); 77 | } else { 78 | $data_msg .= "\t\t$data[1] => ['field_name' => '$data[2]', 'field_type' => '$data[3]', 'scale' => " . (!empty($data[6]) ? "$data[6]" : "1") . ", " 79 | . "'offset' => " . (!empty($data[7]) ? "$data[7]" : "0") . ", " 80 | . "'units' => " . (!empty($data[8]) ? "'$data[8]'" : "''") . ", " 81 | . "'bits' => " . (!empty($data[9]) ? "'$data[9]'" : "''") . ", " 82 | . "'accumulate' => " . (!empty($data[10]) ? "'$data[10]'" : "''") . ", " 83 | . (!empty($data[5]) ? "'component'=>'$data[5]', " : "") 84 | . "'ref_field_type' => " . (!empty($data[12]) ? "'$data[12]'" : "''") . ", " 85 | . "'ref_field_name' => '$data[11]'], " . (!empty($data[13]) ? "// $data[13] (e.g. $data[15])\n" : "\n"); 86 | } 87 | } 88 | } 89 | 90 | $data_msg .= "\n\t]\n]];\n"; 91 | 92 | fclose($open); 93 | } else { 94 | echo "The CSV version of the Profiles.xlsx Messages tab needs to be available in the local directory.\n"; 95 | exit(1); 96 | } 97 | 98 | /** 99 | * Assumes $data array is first message in dynamic message, then scans for more and stops 100 | * @param type $fp 101 | * @param type $data 102 | * @param type $data_msg 103 | */ 104 | function addDynamic($fp, $data, $data_msg) { 105 | 106 | if (empty($data[0]) && empty($data[1]) && empty($data[2])) { 107 | // not a message - abort trying to find it 108 | return $data_msg; 109 | } 110 | 111 | // Trim off previous array closure 112 | $lastClose = strrpos($data_msg, "]"); 113 | $data_msg[$lastClose] = ' '; 114 | $dynamicIdx = 0; 115 | // 116 | // Build Dynamic 117 | // Where some fields have multiple scales, switch to string format "1,1,1,1" etc. 118 | // Also multiple components and ref_field_values will be comma seperated strings 119 | if (str_contains($data[6], ',')) { 120 | $data[6] = "'$data[6]'"; // string of scale values 121 | } 122 | 123 | $data_msg .= "\t\t$dynamicIdx => ['field_name' => '$data[2]', 'field_type' => '$data[3]', " 124 | . "'scale' => " . (!empty($data[6]) ? "$data[6]" : "1") . ", " 125 | . "'offset' => " . (!empty($data[7]) ? "$data[7]" : "0") . ", " 126 | . "'units' => " . (!empty($data[8]) ? "'$data[8]'" : "''") . ", " 127 | . "'bits' => " . (!empty($data[9]) ? "'$data[9]'" : "''") . ", " 128 | . "'accumulate' => " . (!empty($data[10]) ? "'$data[10]'" : "''") . ", " 129 | . (!empty($data[5]) ? "'component'=>'$data[5]', " : "") 130 | . "'ref_field_type' => " . (!empty($data[12]) ? "'$data[12]'" : "''") . ", " 131 | . "'ref_field_name' => '$data[11]'], " . (!empty($data[13]) ? "// $data[13] (e.g. $data[15])\n" : "\n"); 132 | 133 | while (($data = fgetcsv($fp, 2000, ",")) !== FALSE) { 134 | $dynamicIdx++; 135 | if (!is_numeric($data[1]) && empty($data[0])) { 136 | // still a dynamic field and not a new field 137 | if (str_contains($data[6], ',')) { 138 | $data[6] = "'$data[6]'"; // string of scale values 139 | } 140 | $data_msg .= "\t\t$dynamicIdx => ['field_name' => '$data[2]', 'field_type' => '$data[3]', " 141 | . "'scale' => " . (!empty($data[6]) ? "$data[6]" : "1") . ", " 142 | . "'offset' => " . (!empty($data[7]) ? "$data[7]" : "0") . ", " 143 | . "'units' => " . (!empty($data[8]) ? "'$data[8]'" : "''") . ", " 144 | . "'bits' => " . (!empty($data[9]) ? "'$data[9]'" : "''") . ", " 145 | . "'accumulate' => " . (!empty($data[10]) ? "'$data[10]'" : "''") . ", " 146 | . (!empty($data[5]) ? "'component'=>'$data[5]', " : "") 147 | . "'ref_field_type' => " . (!empty($data[12]) ? "'$data[12]'" : "''") . ", " 148 | . "'ref_field_name' => '$data[11]'], " . (!empty($data[13]) ? "// $data[13] (e.g. $data[15])\n" : "\n"); 149 | } else { 150 | // no long dynamic message -> new field or more data 151 | // close previous array 152 | $data_msg .= "\t\t],\n"; 153 | 154 | if (!empty($data[0])) { 155 | // new field 156 | $data_msg .= "],\n\t'$data[0]' => [ " . (!empty($data[13]) ? "// $data[13]\n" : "\n"); 157 | return $data_msg; 158 | } 159 | // new message 160 | $data_msg .= "\t\t$data[1] => ['field_name' => '$data[2]', 'field_type' => '$data[3]', 'scale' => " . (!empty($data[6]) ? "$data[6]" : "1") . ", " 161 | . "'offset' => " . (!empty($data[7]) ? "$data[7]" : "0") . ", " 162 | . "'units' => " . (!empty($data[8]) ? "'$data[8]'" : "''") . ", " 163 | . "'bits' => " . (!empty($data[9]) ? "'$data[9]'" : "''") . ", " 164 | . "'accumulate' => " . (!empty($data[10]) ? "'$data[10]'" : "''") . ", " 165 | . (!empty($data[5]) ? "'component'=>'$data[5]', " : "") 166 | . "'ref_field_type' => " . (!empty($data[12]) ? "'$data[12]'" : "''") . ", " 167 | . "'ref_field_name' => '$data[11]'], " . (!empty($data[13]) ? "// $data[13] (e.g. $data[15])\n" : "\n"); 168 | return $data_msg; 169 | } 170 | } 171 | // file data stream ends with dynamic message as last element 172 | // close previous array 173 | $data_msg .= "\t\t],\n"; 174 | return $data_msg; 175 | } 176 | 177 | $fp = fopen('type-messages.php', 'w'); 178 | fwrite($fp, " [], 18 | // 'units' => 'metric', 19 | // 'pace' => false 20 | ]; 21 | $pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options); 22 | } catch (Exception $e) { 23 | echo 'caught exception: '.$e->getMessage(); 24 | die(); 25 | } 26 | 27 | // Create an array of lat/long coordiantes for the map 28 | $position_lat = $pFFA->data_mesgs['record']['position_lat']; 29 | $position_long = $pFFA->data_mesgs['record']['position_long']; 30 | $lat_long_combined = []; 31 | foreach ($position_lat as $key => $value) { // Assumes every lat has a corresponding long 32 | $lat_long_combined[] = '[' . $position_lat[$key] . ',' . $position_long[$key] . ']'; 33 | } 34 | 35 | // Date with Google timezoneAPI removed 36 | $date = new DateTime('now', new DateTimeZone('UTC')); 37 | $date_s = $pFFA->data_mesgs['session']['start_time']; 38 | $date->setTimestamp($date_s); 39 | 40 | ?> 41 | 42 | 43 | 44 | 45 | phpFITFileAnalysis demo 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |

phpFITFileAnalysis A PHP class for analysing FIT files created by Garmin GPS devices.

54 |

This is a demonstration of the phpFITFileAnalysis class available on GitHub

55 |
56 |
57 |
58 |
59 |
60 |
61 |
File:
62 |
63 |
Device:
64 |
manufacturer() . ' ' . $pFFA->product(); ?>
65 |
Sport:
66 |
sport(); ?>
67 |
68 |
69 |
70 |
71 |
Recorded:
72 |
73 | format('D, d-M-y @ g:ia'); 75 | ?> 76 |
77 |
Duration:
78 |
data_mesgs['session']['total_elapsed_time']); ?>
79 |
Distance:
80 |
data_mesgs['record']['distance']); ?> km
81 |
82 |
83 |
84 |
85 |
86 |
87 |

Messages

88 |
89 |
90 | data_mesgs as $mesg_key => $mesg) { 93 | if ($mesg_key == 'record') { 94 | echo ''; 95 | } 96 | echo $mesg_key.'
'; 97 | if ($mesg_key == 'record') { 98 | echo '
'; 99 | } 100 | } 101 | ?> 102 |
103 |
104 |
105 |
106 |

Record Fields

107 |
108 |
109 | data_mesgs['record'] as $mesg_key => $mesg) { 112 | if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') { 113 | echo ''; 114 | } 115 | echo $mesg_key.'
'; 116 | if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') { 117 | echo '
'; 118 | } 119 | } 120 | ?> 121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |

Flot Charts click

130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |

Leaflet Map

145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | 155 | 156 | 157 | 158 | 243 | 244 | -------------------------------------------------------------------------------- /demo/js/jquery.flot.pie.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){var REDRAW_ATTEMPTS=10;var REDRAW_SHRINK=.95;function init(plot){var canvas=null,target=null,options=null,maxRadius=null,centerLeft=null,centerTop=null,processed=false,ctx=null;var highlights=[];plot.hooks.processOptions.push(function(plot,options){if(options.series.pie.show){options.grid.show=false;if(options.series.pie.label.show=="auto"){if(options.legend.show){options.series.pie.label.show=false}else{options.series.pie.label.show=true}}if(options.series.pie.radius=="auto"){if(options.series.pie.label.show){options.series.pie.radius=3/4}else{options.series.pie.radius=1}}if(options.series.pie.tilt>1){options.series.pie.tilt=1}else if(options.series.pie.tilt<0){options.series.pie.tilt=0}}});plot.hooks.bindEvents.push(function(plot,eventHolder){var options=plot.getOptions();if(options.series.pie.show){if(options.grid.hoverable){eventHolder.unbind("mousemove").mousemove(onMouseMove)}if(options.grid.clickable){eventHolder.unbind("click").click(onClick)}}});plot.hooks.processDatapoints.push(function(plot,series,data,datapoints){var options=plot.getOptions();if(options.series.pie.show){processDatapoints(plot,series,data,datapoints)}});plot.hooks.drawOverlay.push(function(plot,octx){var options=plot.getOptions();if(options.series.pie.show){drawOverlay(plot,octx)}});plot.hooks.draw.push(function(plot,newCtx){var options=plot.getOptions();if(options.series.pie.show){draw(plot,newCtx)}});function processDatapoints(plot,series,datapoints){if(!processed){processed=true;canvas=plot.getCanvas();target=$(canvas).parent();options=plot.getOptions();plot.setData(combine(plot.getData()))}}function combine(data){var total=0,combined=0,numCombined=0,color=options.series.pie.combine.color,newdata=[];for(var i=0;ioptions.series.pie.combine.threshold){newdata.push($.extend(data[i],{data:[[1,value]],color:data[i].color,label:data[i].label,angle:value*Math.PI*2/total,percent:value/(total/100)}))}}if(numCombined>1){newdata.push({data:[[1,combined]],color:color,label:options.series.pie.combine.label,angle:combined*Math.PI*2/total,percent:combined/(total/100)})}return newdata}function draw(plot,newCtx){if(!target){return}var canvasWidth=plot.getPlaceholder().width(),canvasHeight=plot.getPlaceholder().height(),legendWidth=target.children().filter(".legend").children().width()||0;ctx=newCtx;processed=false;maxRadius=Math.min(canvasWidth,canvasHeight/options.series.pie.tilt)/2;centerTop=canvasHeight/2+options.series.pie.offset.top;centerLeft=canvasWidth/2;if(options.series.pie.offset.left=="auto"){if(options.legend.position.match("w")){centerLeft+=legendWidth/2}else{centerLeft-=legendWidth/2}if(centerLeftcanvasWidth-maxRadius){centerLeft=canvasWidth-maxRadius}}else{centerLeft+=options.series.pie.offset.left}var slices=plot.getData(),attempts=0;do{if(attempts>0){maxRadius*=REDRAW_SHRINK}attempts+=1;clear();if(options.series.pie.tilt<=.8){drawShadow()}}while(!drawPie()&&attempts=REDRAW_ATTEMPTS){clear();target.prepend("
Could not draw pie with labels contained inside canvas
")}if(plot.setSeries&&plot.insertLegend){plot.setSeries(slices);plot.insertLegend()}function clear(){ctx.clearRect(0,0,canvasWidth,canvasHeight);target.children().filter(".pieLabel, .pieLabelBackground").remove()}function drawShadow(){var shadowLeft=options.series.pie.shadow.left;var shadowTop=options.series.pie.shadow.top;var edge=10;var alpha=options.series.pie.shadow.alpha;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;if(radius>=canvasWidth/2-shadowLeft||radius*options.series.pie.tilt>=canvasHeight/2-shadowTop||radius<=edge){return}ctx.save();ctx.translate(shadowLeft,shadowTop);ctx.globalAlpha=alpha;ctx.fillStyle="#000";ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);for(var i=1;i<=edge;i++){ctx.beginPath();ctx.arc(0,0,radius,0,Math.PI*2,false);ctx.fill();radius-=i}ctx.restore()}function drawPie(){var startAngle=Math.PI*options.series.pie.startAngle;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;ctx.save();ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);ctx.save();var currentAngle=startAngle;for(var i=0;i0){ctx.save();ctx.lineWidth=options.series.pie.stroke.width;currentAngle=startAngle;for(var i=0;i1e-9){ctx.moveTo(0,0)}ctx.arc(0,0,radius,currentAngle,currentAngle+angle/2,false);ctx.arc(0,0,radius,currentAngle+angle/2,currentAngle+angle,false);ctx.closePath();currentAngle+=angle;if(fill){ctx.fill()}else{ctx.stroke()}}function drawLabels(){var currentAngle=startAngle;var radius=options.series.pie.label.radius>1?options.series.pie.label.radius:maxRadius*options.series.pie.label.radius;for(var i=0;i=options.series.pie.label.threshold*100){if(!drawLabel(slices[i],currentAngle,i)){return false}}currentAngle+=slices[i].angle}return true;function drawLabel(slice,startAngle,index){if(slice.data[0][1]==0){return true}var lf=options.legend.labelFormatter,text,plf=options.series.pie.label.formatter;if(lf){text=lf(slice.label,slice)}else{text=slice.label}if(plf){text=plf(text,slice)}var halfAngle=(startAngle+slice.angle+startAngle)/2;var x=centerLeft+Math.round(Math.cos(halfAngle)*radius);var y=centerTop+Math.round(Math.sin(halfAngle)*radius)*options.series.pie.tilt;var html=""+text+"";target.append(html);var label=target.children("#pieLabel"+index);var labelTop=y-label.height()/2;var labelLeft=x-label.width()/2;label.css("top",labelTop);label.css("left",labelLeft);if(0-labelTop>0||0-labelLeft>0||canvasHeight-(labelTop+label.height())<0||canvasWidth-(labelLeft+label.width())<0){return false}if(options.series.pie.label.background.opacity!=0){var c=options.series.pie.label.background.color;if(c==null){c=slice.color}var pos="top:"+labelTop+"px;left:"+labelLeft+"px;";$("
").css("opacity",options.series.pie.label.background.opacity).insertBefore(label)}return true}}}}function drawDonutHole(layer){if(options.series.pie.innerRadius>0){layer.save();var innerRadius=options.series.pie.innerRadius>1?options.series.pie.innerRadius:maxRadius*options.series.pie.innerRadius;layer.globalCompositeOperation="destination-out";layer.beginPath();layer.fillStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.fill();layer.closePath();layer.restore();layer.save();layer.beginPath();layer.strokeStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.stroke();layer.closePath();layer.restore()}}function isPointInPoly(poly,pt){for(var c=false,i=-1,l=poly.length,j=l-1;++i1?options.series.pie.radius:maxRadius*options.series.pie.radius,x,y;for(var i=0;i1?options.series.pie.radius:maxRadius*options.series.pie.radius;octx.save();octx.translate(centerLeft,centerTop);octx.scale(1,options.series.pie.tilt);for(var i=0;i1e-9){octx.moveTo(0,0)}octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle/2,false);octx.arc(0,0,radius,series.startAngle+series.angle/2,series.startAngle+series.angle,false);octx.closePath();octx.fill()}}}var options={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,shadow:{left:5,top:15,alpha:.02},offset:{top:0,left:"auto"},stroke:{color:"#fff",width:1},label:{show:"auto",formatter:function(label,slice){return"
"+label+"
"+Math.round(slice.percent)+"%
"},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:.5}}}};$.plot.plugins.push({init:init,options:options,name:"pie",version:"1.1"})})(jQuery); -------------------------------------------------------------------------------- /demo/mountain-biking.php: -------------------------------------------------------------------------------- 1 | [], 20 | // 'units' => 'metric', 21 | // 'pace' => false 22 | ]; 23 | $pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options); 24 | } catch (Exception $e) { 25 | echo 'caught exception: '.$e->getMessage(); 26 | die(); 27 | } 28 | 29 | // Google Static Maps API 30 | $position_lat = $pFFA->data_mesgs['record']['position_lat']; 31 | $position_long = $pFFA->data_mesgs['record']['position_long']; 32 | $lat_long_combined = []; 33 | 34 | foreach ($position_lat as $key => $value) { // Assumes every lat has a corresponding long 35 | $lat_long_combined[] = [$position_lat[$key],$position_long[$key]]; 36 | } 37 | 38 | $delta = 0.0001; 39 | do { 40 | $RDP_LatLng_coord = simplify_RDP($lat_long_combined, $delta); // Simplify the array of coordinates using the Ramer-Douglas-Peucker algorithm. 41 | $delta += 0.0001; // Rough accuracy somewhere between 4m and 12m depending where in the World coordinates are, source http://en.wikipedia.org/wiki/Decimal_degrees 42 | 43 | $polylineEncoder = new PolylineEncoder(); // Create an encoded string to pass as the path variable for the Google Static Maps API 44 | foreach ($RDP_LatLng_coord as $RDP) { 45 | $polylineEncoder->addPoint($RDP[0], $RDP[1]); 46 | } 47 | $map_encoded_polyline = $polylineEncoder->encodedString(); 48 | 49 | $map_string = '&path=color:red%7Cenc:'.$map_encoded_polyline; 50 | } while (strlen($map_string) > 1800); // Google Map web service URL limit is 2048 characters. 1800 is arbitrary attempt to stay under 2048 51 | 52 | $LatLng_start = implode(',', $lat_long_combined[0]); 53 | $LatLng_finish = implode(',', $lat_long_combined[count($lat_long_combined)-1]); 54 | 55 | $map_string .= '&markers=color:red%7Clabel:F%7C'.$LatLng_finish.'&markers=color:green%7Clabel:S%7C'.$LatLng_start; 56 | 57 | 58 | // Google Time Zone API 59 | $date = new DateTime('now', new DateTimeZone('UTC')); 60 | $date_s = $pFFA->data_mesgs['session']['start_time']; 61 | 62 | $url_tz = 'https://maps.googleapis.com/maps/api/timezone/json?location='.$LatLng_start.'×tamp='.$date_s.'&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE'; 63 | 64 | $result = file_get_contents($url_tz); 65 | $json_tz = json_decode($result); 66 | if ($json_tz->status == 'OK') { 67 | $date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset; 68 | } else { 69 | $json_tz->timeZoneName = 'Error'; 70 | } 71 | $date->setTimestamp($date_s); 72 | 73 | 74 | // Google Geocoding API 75 | $location = 'Error'; 76 | $url_coord = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='.$LatLng_start.'&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE'; 77 | $result = file_get_contents($url_coord); 78 | $json_coord = json_decode($result); 79 | if ($json_coord->status == 'OK') { 80 | foreach ($json_coord->results[0]->address_components as $addressPart) { 81 | if ((in_array('locality', $addressPart->types)) && (in_array('political', $addressPart->types))) { 82 | $city = $addressPart->long_name; 83 | } elseif ((in_array('administrative_area_level_1', $addressPart->types)) && (in_array('political', $addressPart->types))) { 84 | $state = $addressPart->short_name; 85 | } elseif ((in_array('country', $addressPart->types)) && (in_array('political', $addressPart->types))) { 86 | $country = $addressPart->long_name; 87 | } 88 | } 89 | $location = $city.', '.$state.', '.$country; 90 | } 91 | ?> 92 | 93 | 94 | 95 | 96 | phpFITFileAnalysis demo 97 | 98 | 99 | 100 | 101 |
102 |
103 |

phpFITFileAnalysis A PHP class for analysing FIT files created by Garmin GPS devices.

104 |

This is a demonstration of the phpFITFileAnalysis class available on GitHub

105 |
106 |
107 |
108 |
109 |
110 |
111 |
File:
112 |
113 |
Device:
114 |
manufacturer() . ' ' . $pFFA->product(); ?>
115 |
Sport:
116 |
sport(); ?>
117 |
118 |
119 |
120 |
121 |
Recorded:
122 |
123 | format('D, d-M-y @ g:ia'); 125 | ?> 126 |
127 |
Duration:
128 |
data_mesgs['session']['total_elapsed_time']); ?>
129 |
Distance:
130 |
data_mesgs['record']['distance']); ?> km
131 |
132 |
133 |
134 |
135 |
136 |
137 |

Messages

138 |
139 |
140 | data_mesgs as $mesg_key => $mesg) { 143 | if ($mesg_key == 'record') { 144 | echo ''; 145 | } 146 | echo $mesg_key.'
'; 147 | if ($mesg_key == 'record') { 148 | echo '
'; 149 | } 150 | } 151 | ?> 152 |
153 |
154 |
155 |
156 |

Record Fields

157 |
158 |
159 | data_mesgs['record'] as $mesg_key => $mesg) { 162 | if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') { 163 | echo ''; 164 | } 165 | echo $mesg_key.'
'; 166 | if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') { 167 | echo '
'; 168 | } 169 | } 170 | ?> 171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |

Flot Charts click

180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |

Google Map

195 |
196 |
197 |
198 | Google Geocoding API:
199 | Google Time Zone API: timeZoneName; ?>

200 | Google map 201 |
202 |
203 |
204 |
205 |
206 |
207 |

208 |
209 |
210 |
211 |

Debug Information

212 |
213 |
214 | showDebugInfo(); ?> 215 |
216 |
217 |
218 |
219 | 220 | 221 | 222 | 297 | 298 | -------------------------------------------------------------------------------- /demo/quadrant-analysis.php: -------------------------------------------------------------------------------- 1 | ['all'], 15 | // 'units' => ['metric'] 16 | ]; 17 | $pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options); 18 | 19 | // Google Time Zone API 20 | $date = new DateTime('now', new DateTimeZone('UTC')); 21 | $date_s = $pFFA->data_mesgs['session']['start_time']; 22 | 23 | $url_tz = "https://maps.googleapis.com/maps/api/timezone/json?location=".reset($pFFA->data_mesgs['record']['position_lat']).','.reset($pFFA->data_mesgs['record']['position_long'])."×tamp=".$date_s."&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE"; 24 | 25 | $result = file_get_contents("$url_tz"); 26 | $json_tz = json_decode($result); 27 | if ($json_tz->status == "OK") { 28 | $date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset; 29 | } 30 | $date->setTimestamp($date_s); 31 | 32 | $crank_length = 0.175; 33 | $ftp = 329; 34 | $selected_cadence = 90; 35 | 36 | $json = $pFFA->getJSON($crank_length, $ftp, ['all'], $selected_cadence); 37 | 38 | } catch (Exception $e) { 39 | echo 'caught exception: '.$e->getMessage(); 40 | die(); 41 | } 42 | ?> 43 | 44 | 45 | 46 | 47 | phpFITFileAnalysis demo 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |

phpFITFileAnalysis A PHP class for analysing FIT files created by Garmin GPS devices.

56 |

This is a demonstration of the phpFITFileAnalysis class available on GitHub

57 |
58 |
59 |
60 |
61 |
62 |
File:
63 |
64 |
Device:
65 |
manufacturer() . ' ' . $pFFA->product(); ?>
66 |
Sport:
67 |
sport(); ?>
68 |
69 |
70 |
71 |
72 |
Recorded:
73 |
format('D, d-M-y @ g:ia'); ?> 74 |
75 |
Duration:
76 |
data_mesgs['session']['total_elapsed_time']); ?>
77 |
Distance:
78 |
data_mesgs['record']['distance']); ?> km
79 |
80 |
81 | 82 |
83 |
84 |
85 |
86 |

87 | Quadrant Analysis 88 | 89 |

90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |

Google Map

100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | 110 |
111 |
112 |
113 |
114 |

Laps

115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |

Cadence histogram

125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |

Heart Rate histogram

135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | 143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 355 | 356 | -------------------------------------------------------------------------------- /demo/power-analysis.php: -------------------------------------------------------------------------------- 1 | ['all'], 15 | // 'units' => ['metric'] 16 | ]; 17 | $pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options); 18 | 19 | // Google Time Zone API 20 | $date = new DateTime('now', new DateTimeZone('UTC')); 21 | $date_s = $pFFA->data_mesgs['session']['start_time']; 22 | 23 | $url_tz = "https://maps.googleapis.com/maps/api/timezone/json?location=".reset($pFFA->data_mesgs['record']['position_lat']).','.reset($pFFA->data_mesgs['record']['position_long'])."×tamp=".$date_s."&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE"; 24 | 25 | $result = file_get_contents("$url_tz"); 26 | $json_tz = json_decode($result); 27 | if ($json_tz->status == "OK") { 28 | $date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset; 29 | } 30 | $date->setTimestamp($date_s); 31 | 32 | $ftp = 329; 33 | $hr_metrics = $pFFA->hrMetrics(52, 185, 172, 'male'); 34 | $power_metrics = $pFFA->powerMetrics($ftp); 35 | $criticalPower = $pFFA->criticalPower([2,3,5,10,30,60,120,300,600,1200,3600,7200,10800,18000]); 36 | $power_histogram = $pFFA->powerHistogram(); 37 | $power_table = $pFFA->powerPartioned($ftp); 38 | $power_pie_chart = $pFFA->partitionData('power', $pFFA->powerZones($ftp), true, false); 39 | $quad_plot = $pFFA->quadrantAnalysis(0.175, $ftp); 40 | } catch (Exception $e) { 41 | echo 'caught exception: '.$e->getMessage(); 42 | die(); 43 | } 44 | ?> 45 | 46 | 47 | 48 | 49 | phpFITFileAnalysis demo 50 | 51 | 52 | 53 | 54 |
55 |
56 |

phpFITFileAnalysis A PHP class for analysing FIT files created by Garmin GPS devices.

57 |

This is a demonstration of the phpFITFileAnalysis class available on GitHub

58 |
59 |
60 |
61 |
62 |
63 |
File:
64 |
65 |
Device:
66 |
manufacturer() . ' ' . $pFFA->product(); ?>
67 |
Sport:
68 |
sport(); ?>
69 |
70 |
71 |
72 |
73 |
Recorded:
74 |
format('D, d-M-y @ g:ia'); ?> 75 |
76 |
Duration:
77 |
data_mesgs['session']['total_elapsed_time']); ?>
78 |
Distance:
79 |
data_mesgs['record']['distance']); ?> km
80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 |

Metrics

88 |
89 |
90 |
91 |

Power

92 | $value) { 94 | echo "$key: $value
"; 95 | } 96 | ?> 97 |
98 |
99 |

Heart Rate

100 | $value) { 102 | echo "$key: $value
"; 103 | } 104 | ?> 105 |
106 |
107 |
108 | 109 |
110 |
111 |

Critical Power

112 |
113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 |

Power Distribution (histogram)

121 |
122 |
123 |
124 |
125 |
126 | 127 |
128 |
129 |

Power Zones

130 |
131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | $value) { 143 | echo ''; 144 | echo ''; 145 | echo ''; 146 | } 147 | ?> 148 | 149 | 150 |
ZoneZone range% in zone
'.$i++.''.$key.' w'.$value.' %
151 |
152 |
153 |
154 |
155 |
156 |
157 | 158 |
159 |
160 |

Quadrant Analysis Circumferential Pedal Velocity (x-axis) vs Average Effective Pedal Force (y-axis)

161 |
162 |
163 |
164 |
165 |
166 | 167 |
168 |
169 |
170 | 171 | 172 | 173 | 174 | 489 | 490 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/adriangibbons/php-fit-file-analysis.svg?branch=master)](https://travis-ci.org/adriangibbons/php-fit-file-analysis) [![Packagist](https://img.shields.io/packagist/v/adriangibbons/php-fit-file-analysis.svg)](https://packagist.org/packages/adriangibbons/php-fit-file-analysis) [![Packagist](https://img.shields.io/packagist/dt/adriangibbons/php-fit-file-analysis.svg)](https://packagist.org/packages/adriangibbons/php-fit-file-analysis) [![Coverage Status](https://coveralls.io/repos/adriangibbons/php-fit-file-analysis/badge.svg?branch=master&service=github)](https://coveralls.io/github/adriangibbons/php-fit-file-analysis?branch=master) 2 | # phpFITFileAnalysis 3 | 4 | A PHP (>= v5.4) class for analysing FIT files created by Garmin GPS devices. 5 | 6 | ## Changes from main branch in this repository 7 | Added some enumeration support up to FIT SDK Version: 21.101 including addition of dynamic fields. However there are several newer FIT features that have not been addressed yet. It does fix some of the reported issues on the main branch. 8 | Added a timeInZones heart rate calculations and some meta data to the hrMetrics method 9 | Added using CDN for bootstrap so the showDebugInfo() routine displays with proper formatting with no extra work. 10 | Still a work in progress as I add dynamic messages but should be backwards compatible and handle more FIT files. 11 | 12 | Note, one behavorial change is if there are no records or recorded data found in the fit file, it will report no records found instead of failing silently. 13 | 14 | ## New Utilities 15 | Created a utility (readProfileCSV.php) to parse the FIT Profile.xlsx sheets directly into usable arrays for the class. The spread sheet found in the garmin SDK https://developer.garmin.com/fit/download/ should be saved as 2 files "Profile-mesg.csv" for the message tab and "Profile-types.csv" for the type tab. Then in the same directory run ```php readProfileCSV.php``` to extract type-enum.php and type-message.php. These can then be copied into the main phpFitFileAnalsis.php class. Don't forget to declare them private when imported to the class. 16 | 17 | ## Demo Screenshots 18 | [Live demonstration](http://adriangibbons.com/php-fit-file-analysis/demo/) (Right-click and Open in new tab) 19 | 20 | ![Mountain Biking](demo/img/mountain-biking.jpg) 21 | ![Power Analysis](demo/img/power-analysis.jpg) 22 | ![Quadrant Analysis](demo/img/quadrant-analysis.jpg) 23 | ![Swim](demo/img/swim.jpg) 24 | 25 | Please read this page in its entirety and the [FAQ](https://github.com/adriangibbons/php-fit-file-analysis/wiki/Frequently-Asked-Questions-(FAQ)) first if you have any questions or need support. 26 | 27 | ## What is a FIT file? 28 | FIT or Flexible and Interoperable Data Transfer is a file format used for GPS tracks and routes. It is used by newer Garmin fitness GPS devices, including the Edge and Forerunner series, which are popular with cyclists and runners. 29 | 30 | Visit the FAQ page within the Wiki for more information. 31 | 32 | ## How do I use phpFITFileAnalysis with my PHP-driven website? 33 | 34 | A couple of choices here: 35 | 36 | **The more modern way:** Add the package *adriangibbons/php-fit-file-analysis* in a composer.json file: 37 | ```JSON 38 | { 39 | "require": { 40 | "adriangibbons/php-fit-file-analysis": "^3.2.0" 41 | } 42 | } 43 | ``` 44 | Run ```composer update``` from the command line. 45 | 46 | The composer.json file should autoload the ```phpFITFileAnalysis``` class, so as long as you include the autoload file in your PHP file, you should be able to instantiate the class with: 47 | ```php 48 | 52 | ``` 53 | 54 | **The more manual way:** Download the ZIP from GitHub and put PHP class file from the /src directory somewhere appropriate (e.g. classes/). A conscious effort has been made to keep everything in a single file. 55 | 56 | Then include the file on the PHP page where you want to use it and instantiate an object of the class: 57 | ```php 58 | 62 | ``` 63 | Note that the only mandatory parameter required when creating an instance is the path to the FIT file that you want to load. 64 | 65 | There are more **Optional Parameters** that can be supplied. These are described in more detail further down this page. 66 | 67 | The object will automatically load the FIT file and iterate through its contents. It will store any data it finds in arrays, which are accessible via the public data variable. 68 | 69 | ### Accessing the Data 70 | Data read by the class are stored in associative arrays, which are accessible via the public data variable: 71 | ```php 72 | $pFFA->data_mesgs 73 | ``` 74 | The array indexes are the names of the messages and fields that they contain. For example: 75 | ```php 76 | // Contains an array of all heart_rate data read from the file, indexed by timestamp 77 | $pFFA->data_mesgs['record']['heart_rate'] 78 | // Contains an integer identifying the number of laps 79 | $pFFA->data_mesgs['session']['num_laps'] 80 | ``` 81 | **OK, but how do I know what messages and fields are in my file?** 82 | You could either iterate through the $pFFA->data_mesgs array, or take a look at the debug information you can dump to a webpage: 83 | ```php 84 | // Option 1. Iterate through the $pFFA->data_mesgs array 85 | foreach ($pFFA->data_mesgs as $mesg_key => $mesg) { // Iterate the array and output the messages 86 | echo "Found Message: $mesg_key
"; 87 | foreach ($mesg as $field_key => $field) { // Iterate each message and output the fields 88 | echo " - Found Field: $mesg_key -> $field_key
"; 89 | } 90 | echo "
"; 91 | } 92 | 93 | // Option 2. Show the debug information 94 | $pFFA->showDebugInfo(); // Quite a lot of info... 95 | ``` 96 | **How about some real-world examples?** 97 | ```php 98 | // Get Max and Avg Speed 99 | echo "Maximum Speed: ".max($pFFA->data_mesgs['record']['speed'])."
"; 100 | echo "Average Speed: ".( array_sum($pFFA->data_mesgs['record']['speed']) / count($pFFA->data_mesgs['record']['speed']) )."
"; 101 | 102 | // Put HR data into a JavaScript array for use in a Chart 103 | echo "var chartData = ["; 104 | foreach ($pFFA->data_mesgs['record']['heart_rate'] as $timestamp => $hr_value) { 105 | echo "[$timestamp,$hr_value],"; 106 | } 107 | echo "];"; 108 | ``` 109 | **Enumerated Data** 110 | The FIT protocol makes use of enumerated data types. Where these values have been identified in the FIT SDK, they have been included in the class as a private variable: $enum_data. 111 | 112 | A public function is available, which will return the enumerated value for a given message type. For example: 113 | ```php 114 | // Access data stored within the private class variable $enum_data 115 | // $pFFA->enumData($type, $value) 116 | // e.g. 117 | echo $pFFA->enumData('sport', 2)); // returns 'cycling' 118 | echo $pFFA->enumData('manufacturer', $this->data_mesgs['device_info']['manufacturer']); // returns 'Garmin'; 119 | echo $pFFA->manufacturer(); // Short-hand for above 120 | ``` 121 | In addition, public functions provide a short-hand way to access commonly used enumerated data: 122 | 123 | - manufacturer() 124 | - product() 125 | - sport() 126 | 127 | ### Optional Parameters 128 | There are five optional parameters that can be passed as an associative array when the phpFITFileAnalysis object is instantiated. These are: 129 | 130 | - fix_data 131 | - data_every_second 132 | - units 133 | - pace 134 | - garmin_timestamps 135 | - overwrite_with_dev_data 136 | 137 | For example: 138 | ```php 139 | $options = [ 140 | 'fix_data' => ['cadence', 'distance'], 141 | 'data_every_second' => true 142 | 'units' => 'statute', 143 | 'pace' => true, 144 | 'garmin_timestamps' => true, 145 | 'overwrite_with_dev_data' => false 146 | ]; 147 | $pFFA = new adriangibbons\phpFITFileAnalysis('my_fit_file.fit', $options); 148 | ``` 149 | The optional parameters are described in more detail below. 150 | #### "Fix" the Data 151 | FIT files have been observed where some data points are missing for one sensor (e.g. cadence/foot pod), where information has been collected for other sensors (e.g. heart rate) at the same instant. The cause is unknown and typically only a relatively small number of data points are missing. Fixing the issue is probably unnecessary, as each datum is indexed using a timestamp. However, it may be important for your project to have the exact same number of data points for each type of data. 152 | 153 | **Recognised values:** 'all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'power', 'speed' 154 | 155 | **Examples: ** 156 | ```php 157 | $options = ['fix_data' => ['all']]; // fix cadence, distance, heart_rate, lat_lon, power, and speed data 158 | $options = ['fix_data' => ['cadence', 'distance']]; // fix cadence and distance data only 159 | $options = ['fix_data' => ['lat_lon']]; // fix position data only 160 | ``` 161 | If the *fix_data* array is not supplied, then no "fixing" of the data is performed. 162 | 163 | A FIT file might contain the following: 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |
# Data PointsDelta (c.f. Timestamps)
timestamp102510
position_lat1023625
position_long1023625
altitude102510
heart_rate102510
cadence9716535
distance1023625
speed1023625
power102429
temperature102510
204 | 205 | As illustrated above, the types of data most susceptible to missing data points are: position_lat, position_long, altitude, heart_rate, cadence, distance, speed, and power. 206 | 207 | With the exception of cadence information, missing data points are "fixed" by inserting interpolated values. 208 | 209 | For cadence, zeroes are inserted as it is thought that it is likely no data has been collected due to a lack of movement at that point in time. 210 | 211 | **Interpolation of missing data points** 212 | ```php 213 | // Do not use code, just for demonstration purposes 214 | var_dump($pFFA->data_mesgs['record']['temperature']); // ['100'=>22, '101'=>22, '102'=>23, '103'=>23, '104'=>23]; 215 | var_dump($pFFA->data_mesgs['record']['distance']); // ['100'=>3.62, '101'=>4.01, '104'=>10.88]; 216 | ``` 217 | As you can see from the trivial example above, temperature data have been recorded for each of five timestamps (100, 101, 102, 103, and 104). However, distance information has not been recorded for timestamps 102 and 103. 218 | 219 | If *fix_data* includes 'distance', then the class will attempt to insert data into the distance array with the indexes 102 and 103. Values are determined using a linear interpolation between indexes 101(4.01) and 104(10.88). 220 | 221 | The result would be: 222 | ```php 223 | var_dump($pFFA->data_mesgs['record']['distance']); // ['100'=>3.62, '101'=>4.01, '102'=>6.30, '103'=>8.59, '104'=>10.88]; 224 | ``` 225 | 226 | #### Data Every Second 227 | Some of Garmin's Fitness devices offer the choice of Smart Recording or Every Second Recording. 228 | 229 | Smart Recording records key points where the fitness device changes direction, speed, heart rate or elevation. This recording type records less track points and will potentially have gaps between timestamps of greater than one second. 230 | 231 | You can force timestamps to be regular one second intervals by setting the option: 232 | ```php 233 | $options = ['data_every_second' => true]; 234 | ``` 235 | Missing timestamps will have data interpolated as per the ```fix_data``` option above. 236 | 237 | If the ```fix_data``` option is not specified in conjunction with ```data_every_second``` then ```'fix_data' => ['all']``` is assumed. 238 | 239 | *Note that you may experience degraded performance using the ```fix_data``` option. Improving the performance will be explored - it is likely the ```interpolateMissingData()``` function is sub-optimal.* 240 | 241 | #### Set Units 242 | By default, **metric** units (identified in the table below) are assumed. 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 |
Metric
(DEFAULT)
StatuteRaw
Speedkilometers per hourmiles per hourmeters per second
Distancekilometersmilesmeters
Altitudemetersfeetmeters
Latitudedegreesdegreessemicircles
Longitudedegreesdegreessemicircles
Temperaturecelsius (℃)fahrenheit (℉)celsius (℃)
272 | 273 | You can request **statute** or **raw** units instead of metric. Raw units are those were used by the device that created the FIT file and are native to the FIT standard (i.e. no transformation of values read from the file will occur). 274 | 275 | To select the units you require, use one of the following: 276 | ```php 277 | $options = ['units' => 'statute']; 278 | $options = ['units' => 'raw']; 279 | $options = ['units' => 'metric']; // explicit but not necessary, same as default 280 | ``` 281 | #### Pace 282 | If required by the user, pace can be provided instead of speed. Depending on the units requested, pace will either be in minutes per kilometre (min/km) for metric units; or minutes per mile (min/mi) for statute. 283 | 284 | To select pace, use the following option: 285 | ```php 286 | $options = ['pace' => true]; 287 | ``` 288 | Pace values will be decimal minutes. To get the seconds, you may wish to do something like: 289 | ```php 290 | foreach ($pFFA->data_mesgs['record']['speed'] as $key => $value) { 291 | $min = floor($value); 292 | $sec = round(60 * ($value - $min)); 293 | echo "pace: $min min $sec sec
"; 294 | } 295 | ``` 296 | Note that if 'raw' units are requested then this parameter has no effect on the speed data, as it is left untouched from what was read-in from the file. 297 | 298 | #### Timestamps 299 | Unix time is the number of seconds since **UTC 00:00:00 Jan 01 1970**, however the FIT standard specifies that timestamps (i.e. fields of type date_time and local_date_time) represent seconds since **UTC 00:00:00 Dec 31 1989**. 300 | 301 | The difference (in seconds) between FIT and Unix timestamps is 631,065,600: 302 | ```php 303 | $date_FIT = new DateTime('1989-12-31 00:00:00', new DateTimeZone('UTC')); 304 | $date_UNIX = new DateTime('1970-01-01 00:00:00', new DateTimeZone('UTC')); 305 | $diff = $date_FIT->getTimestamp() - $date_UNIX->getTimestamp(); 306 | echo 'The difference (in seconds) between FIT and Unix timestamps is '. number_format($diff); 307 | ``` 308 | By default, fields of type date_time and local_date_time read from FIT files will have this delta added to them so that they can be treated as Unix time. If the FIT timestamp is required, the 'garmin_timestamps' option can be set to true. 309 | 310 | #### Overwrite with Developer Data 311 | The FIT standard allows developers to define the meaning of data without requiring changes to the FIT profile being used. They may define data that is already incorporated in the standard - e.g. HR, cadence, power, etc. By default, if developers do this, the data will overwrite anything in the regular ```$pFFA->data_mesgs['record']``` array. If you do not want this occur, set the 'overwrite_with_dev_data' option to false. The data will still be available in ```$pFFA->data_mesgs['developer_data']```. 312 | 313 | ## Analysis 314 | The following functions return arrays of data that could be used to create tables/charts: 315 | ```php 316 | array $pFFA->hrPartionedHRmaximum(int $hr_maximum); 317 | array $pFFA->hrPartionedHRreserve(int $hr_resting, int $hr_maximum); 318 | array $pFFA->powerPartioned(int $functional_threshold_power); 319 | array $pFFA->powerHistogram(int $bucket_width = 25); 320 | ``` 321 | For advanced control over these functions, or use with other sensor data (e.g. cadence or speed), use the underlying functions: 322 | ```php 323 | array $pFFA->partitionData(string $record_field='', $thresholds=null, bool $percentages = true, bool $labels_for_keys = true); 324 | array $pFFA->histogram(int $bucket_width=25, string $record_field=''); 325 | ``` 326 | Functions exist to determine thresholds based on percentages of user-supplied data: 327 | ```php 328 | array $pFFA->hrZonesMax(int $hr_maximum, array $percentages_array=[0.60, 0.75, 0.85, 0.95]); 329 | array $pFFA->hrZonesReserve(int $hr_resting, int $hr_maximum, array $percentages_array=[0.60, 0.65, 0.75, 0.82, 0.89, 0.94 ]) { 330 | array $pFFA->powerZones(int $functional_threshold_power, array $percentages_array=[0.55, 0.75, 0.90, 1.05, 1.20, 1.50]); 331 | ``` 332 | ### Heart Rate 333 | A function exists for analysing heart rate data: 334 | ```php 335 | // hr_FT is heart rate at Functional Threshold, or Lactate Threshold Heart Rate 336 | array $pFFA->hrMetrics(int $hr_resting, int $hr_maximum, string $hr_FT, $gender); 337 | // e.g. $pFFA->hrMetrics(52, 189, 172, 'male'); 338 | ``` 339 | **Heart Rate metrics:** 340 | * TRIMP (TRaining IMPulse) 341 | * Intensity Factor 342 | 343 | ### Power 344 | Three functions exist for analysing power data: 345 | ```php 346 | array $pFFA->powerMetrics(int $functional_threshold_power); 347 | array $pFFA->criticalPower(int or array $time_periods); // e.g. 300 or [600, 900] 348 | array $pFFA->quadrantAnalysis(float $crank_length, int $ftp, int $selected_cadence = 90, bool $use_timestamps = false); // Crank length in metres 349 | ``` 350 | **Power metrics:** 351 | * Average Power 352 | * Kilojoules 353 | * Normalised Power (estimate had your power output been constant) 354 | * Variability Index (ratio of Normalised Power / Average Power) 355 | * Intensity Factor (ratio of Normalised Power / Functional Threshold Power) 356 | * Training Stress Score (effort based on relative intensity and duration) 357 | 358 | **Critical Power** (or Best Effort) is the highest average power sustained for a specified period of time within the activity. You can supply a single time period (in seconds), or an array or time periods. 359 | 360 | **Quadrant Analysis** provides insight into the neuromuscular demands of a bike ride through comparing pedal velocity with force by looking at cadence and power. 361 | 362 | Note that ```$pFFA->criticalPower``` and some power metrics (Normalised Power, Variability Index, Intensity Factor, Training Stress Score) will use the [PHP Trader](http://php.net/manual/en/book.trader.php) extension if it is loaded on the server. If the extension is not loaded then it will use the built-in Simple Moving Average algorithm, which is far less performant particularly for larger files! 363 | 364 | A demo of power analysis is available [here](http://adriangibbons.com/php-fit-file-analysis/demo/power-analysis.php). 365 | 366 | ## Other methods 367 | Returns array of booleans using timestamp as key. true == timer paused (e.g. autopause): 368 | ```php 369 | array isPaused() 370 | ``` 371 | Returns a JSON object with requested ride data: 372 | ```php 373 | array getJSON(float $crank_length = null, int $ftp = null, array $data_required = ['all'], int $selected_cadence = 90) 374 | /** 375 | * $data_required can be ['all'] or a combination of: 376 | * ['timestamp', 'paused', 'temperature', 'lap', 'position_lat', 'position_long', 'distance', 'altitude', 'speed', 'heart_rate', 'cadence', 'power', 'quadrant-analysis'] 377 | */ 378 | ``` 379 | Returns array of gear change information (if present, e.g. using Shimano D-Fly Wireless Di2 Transmitter): 380 | ```php 381 | // By default, time spent in a gear whilst the timer is paused (e.g. autopause) is ignored. Set to false to include. 382 | array gearChanges($bIgnoreTimerPaused = true) 383 | ``` 384 | 385 | ## Acknowledgement 386 | This class has been created using information available in a Software Development Kit (SDK) made available by ANT ([thisisant.com](http://www.thisisant.com/resources/fit)). 387 | 388 | As a minimum, I'd recommend reading the three PDFs included in the SDK: 389 | 390 | 1. FIT File Types Description 391 | 2. FIT SDK Introductory Guide 392 | 3. Flexible & Interoperable Data Transfer (FIT) Protocol 393 | 394 | Following these, the 'Profile.xls' spreadsheet and then the Java/C/C++ examples. 395 | -------------------------------------------------------------------------------- /demo/js/jquery.flot.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function($){var hasOwnProperty=Object.prototype.hasOwnProperty;if(!$.fn.detach){$.fn.detach=function(){return this.each(function(){if(this.parentNode){this.parentNode.removeChild(this)}})}}function Canvas(cls,container){var element=container.children("."+cls)[0];if(element==null){element=document.createElement("canvas");element.className=cls;$(element).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(container);if(!element.getContext){if(window.G_vmlCanvasManager){element=window.G_vmlCanvasManager.initElement(element)}else{throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.")}}}this.element=element;var context=this.context=element.getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;this.pixelRatio=devicePixelRatio/backingStoreRatio;this.resize(container.width(),container.height());this.textContainer=null;this.text={};this._textCache={}}Canvas.prototype.resize=function(width,height){if(width<=0||height<=0){throw new Error("Invalid dimensions for plot, width = "+width+", height = "+height)}var element=this.element,context=this.context,pixelRatio=this.pixelRatio;if(this.width!=width){element.width=width*pixelRatio;element.style.width=width+"px";this.width=width}if(this.height!=height){element.height=height*pixelRatio;element.style.height=height+"px";this.height=height}context.restore();context.save();context.scale(pixelRatio,pixelRatio)};Canvas.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)};Canvas.prototype.render=function(){var cache=this._textCache;for(var layerKey in cache){if(hasOwnProperty.call(cache,layerKey)){var layer=this.getTextLayer(layerKey),layerCache=cache[layerKey];layer.hide();for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){if(position.active){if(!position.rendered){layer.append(position.element);position.rendered=true}}else{positions.splice(i--,1);if(position.rendered){position.element.detach()}}}if(positions.length==0){delete styleCache[key]}}}}}layer.show()}}};Canvas.prototype.getTextLayer=function(classes){var layer=this.text[classes];if(layer==null){if(this.textContainer==null){this.textContainer=$("
").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)}layer=this.text[classes]=$("
").addClass(classes).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)}return layer};Canvas.prototype.getTextInfo=function(layer,text,font,angle,width){var textStyle,layerCache,styleCache,info;text=""+text;if(typeof font==="object"){textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px/"+font.lineHeight+"px "+font.family}else{textStyle=font}layerCache=this._textCache[layer];if(layerCache==null){layerCache=this._textCache[layer]={}}styleCache=layerCache[textStyle];if(styleCache==null){styleCache=layerCache[textStyle]={}}info=styleCache[text];if(info==null){var element=$("
").html(text).css({position:"absolute","max-width":width,top:-9999}).appendTo(this.getTextLayer(layer));if(typeof font==="object"){element.css({font:textStyle,color:font.color})}else if(typeof font==="string"){element.addClass(font)}info=styleCache[text]={width:element.outerWidth(true),height:element.outerHeight(true),element:element,positions:[]};element.detach()}return info};Canvas.prototype.addText=function(layer,x,y,text,font,angle,width,halign,valign){var info=this.getTextInfo(layer,text,font,angle,width),positions=info.positions;if(halign=="center"){x-=info.width/2}else if(halign=="right"){x-=info.width}if(valign=="middle"){y-=info.height/2}else if(valign=="bottom"){y-=info.height}for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=true;return}}position={active:true,rendered:false,element:positions.length?info.element.clone():info.element,x:x,y:y};positions.push(position);position.element.css({top:Math.round(y),left:Math.round(x),"text-align":halign})};Canvas.prototype.removeText=function(layer,x,y,text,font,angle){if(text==null){var layerCache=this._textCache[layer];if(layerCache!=null){for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){position.active=false}}}}}}}else{var positions=this.getTextInfo(layer,text,font,angle).positions;for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=false}}}};function Plot(placeholder,data_,options_,plugins){var series=[],options={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:.85,sorted:null},xaxis:{show:null,position:"bottom",mode:null,font:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null},yaxis:{autoscaleMargin:.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false,zero:true},shadowSize:3,highlightColor:null},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,margin:0,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},interaction:{redrawOverlayInterval:1e3/60},hooks:{}},surface=null,overlay=null,eventHolder=null,ctx=null,octx=null,xaxes=[],yaxes=[],plotOffset={left:0,right:0,top:0,bottom:0},plotWidth=0,plotHeight=0,hooks={processOptions:[],processRawData:[],processDatapoints:[],processOffset:[],drawBackground:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},plot=this;plot.setData=setData;plot.setupGrid=setupGrid;plot.draw=draw;plot.getPlaceholder=function(){return placeholder};plot.getCanvas=function(){return surface.element};plot.getPlotOffset=function(){return plotOffset};plot.width=function(){return plotWidth};plot.height=function(){return plotHeight};plot.offset=function(){var o=eventHolder.offset();o.left+=plotOffset.left;o.top+=plotOffset.top;return o};plot.getData=function(){return series};plot.getAxes=function(){var res={},i;$.each(xaxes.concat(yaxes),function(_,axis){if(axis)res[axis.direction+(axis.n!=1?axis.n:"")+"axis"]=axis});return res};plot.getXAxes=function(){return xaxes};plot.getYAxes=function(){return yaxes};plot.c2p=canvasToAxisCoords;plot.p2c=axisToCanvasCoords;plot.getOptions=function(){return options};plot.highlight=highlight;plot.unhighlight=unhighlight;plot.triggerRedrawOverlay=triggerRedrawOverlay;plot.pointOffset=function(point){return{left:parseInt(xaxes[axisNumber(point,"x")-1].p2c(+point.x)+plotOffset.left,10),top:parseInt(yaxes[axisNumber(point,"y")-1].p2c(+point.y)+plotOffset.top,10)}};plot.shutdown=shutdown;plot.destroy=function(){shutdown();placeholder.removeData("plot").empty();series=[];options=null;surface=null;overlay=null;eventHolder=null;ctx=null;octx=null;xaxes=[];yaxes=[];hooks=null;highlights=[];plot=null};plot.resize=function(){var width=placeholder.width(),height=placeholder.height();surface.resize(width,height);overlay.resize(width,height)};plot.hooks=hooks;initPlugins(plot);parseOptions(options_);setupCanvases();setData(data_);setupGrid();draw();bindEvents();function executeHooks(hook,args){args=[plot].concat(args);for(var i=0;imaxIndex){maxIndex=sc}}}if(neededColors<=maxIndex){neededColors=maxIndex+1}var c,colors=[],colorPool=options.colors,colorPoolSize=colorPool.length,variation=0;for(i=0;i=0){if(variation<.5){variation=-variation-.2}else variation=0}else variation=-variation}colors[i]=c.scale("rgb",1+variation)}var colori=0,s;for(i=0;iaxis.datamax&&max!=fakeInfinity)axis.datamax=max}$.each(allAxes(),function(_,axis){axis.datamin=topSentry;axis.datamax=bottomSentry;axis.used=false});for(i=0;i0&&points[k-ps]!=null&&points[k-ps]!=points[k]&&points[k-ps+1]!=points[k+1]){for(m=0;mxmax)xmax=val}if(f.y){if(valymax)ymax=val}}}if(s.bars.show){var delta;switch(s.bars.align){case"left":delta=0;break;case"right":delta=-s.bars.barWidth;break;default:delta=-s.bars.barWidth/2}if(s.bars.horizontal){ymin+=delta;ymax+=delta+s.bars.barWidth}else{xmin+=delta;xmax+=delta+s.bars.barWidth}}updateAxis(s.xaxis,xmin,xmax);updateAxis(s.yaxis,ymin,ymax)}$.each(allAxes(),function(_,axis){if(axis.datamin==topSentry)axis.datamin=null;if(axis.datamax==bottomSentry)axis.datamax=null})}function setupCanvases(){placeholder.css("padding",0).children().filter(function(){return!$(this).hasClass("flot-overlay")&&!$(this).hasClass("flot-base")}).remove();if(placeholder.css("position")=="static")placeholder.css("position","relative");surface=new Canvas("flot-base",placeholder);overlay=new Canvas("flot-overlay",placeholder);ctx=surface.context;octx=overlay.context;eventHolder=$(overlay.element).unbind();var existing=placeholder.data("plot");if(existing){existing.shutdown();overlay.clear()}placeholder.data("plot",plot)}function bindEvents(){if(options.grid.hoverable){eventHolder.mousemove(onMouseMove);eventHolder.bind("mouseleave",onMouseLeave)}if(options.grid.clickable)eventHolder.click(onClick);executeHooks(hooks.bindEvents,[eventHolder])}function shutdown(){if(redrawTimeout)clearTimeout(redrawTimeout);eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mouseleave",onMouseLeave);eventHolder.unbind("click",onClick);executeHooks(hooks.shutdown,[eventHolder])}function setTransformationHelpers(axis){function identity(x){return x}var s,m,t=axis.options.transform||identity,it=axis.options.inverseTransform;if(axis.direction=="x"){s=axis.scale=plotWidth/Math.abs(t(axis.max)-t(axis.min));m=Math.min(t(axis.max),t(axis.min))}else{s=axis.scale=plotHeight/Math.abs(t(axis.max)-t(axis.min));s=-s;m=Math.max(t(axis.max),t(axis.min))}if(t==identity)axis.p2c=function(p){return(p-m)*s};else axis.p2c=function(p){return(t(p)-m)*s};if(!it)axis.c2p=function(c){return m+c/s};else axis.c2p=function(c){return it(m+c/s)}}function measureTickLabels(axis){var opts=axis.options,ticks=axis.ticks||[],labelWidth=opts.labelWidth||0,labelHeight=opts.labelHeight||0,maxWidth=labelWidth||(axis.direction=="x"?Math.floor(surface.width/(ticks.length||1)):null),legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=opts.font||"flot-tick-label tickLabel";for(var i=0;i=0;--i)allocateAxisBoxFirstPhase(allocatedAxes[i]);adjustLayoutForThingsStickingOut();$.each(allocatedAxes,function(_,axis){allocateAxisBoxSecondPhase(axis)})}plotWidth=surface.width-plotOffset.left-plotOffset.right;plotHeight=surface.height-plotOffset.bottom-plotOffset.top;$.each(axes,function(_,axis){setTransformationHelpers(axis)});if(showGrid){drawAxisLabels()}insertLegend()}function setRange(axis){var opts=axis.options,min=+(opts.min!=null?opts.min:axis.datamin),max=+(opts.max!=null?opts.max:axis.datamax),delta=max-min;if(delta==0){var widen=max==0?1:.01;if(opts.min==null)min-=widen;if(opts.max==null||opts.min!=null)max+=widen}else{var margin=opts.autoscaleMargin;if(margin!=null){if(opts.min==null){min-=delta*margin;if(min<0&&axis.datamin!=null&&axis.datamin>=0)min=0}if(opts.max==null){max+=delta*margin;if(max>0&&axis.datamax!=null&&axis.datamax<=0)max=0}}}axis.min=min;axis.max=max}function setupTickGeneration(axis){var opts=axis.options;var noTicks;if(typeof opts.ticks=="number"&&opts.ticks>0)noTicks=opts.ticks;else noTicks=.3*Math.sqrt(axis.direction=="x"?surface.width:surface.height);var delta=(axis.max-axis.min)/noTicks,dec=-Math.floor(Math.log(delta)/Math.LN10),maxDec=opts.tickDecimals;if(maxDec!=null&&dec>maxDec){dec=maxDec}var magn=Math.pow(10,-dec),norm=delta/magn,size;if(norm<1.5){size=1}else if(norm<3){size=2;if(norm>2.25&&(maxDec==null||dec+1<=maxDec)){size=2.5;++dec}}else if(norm<7.5){size=5}else{size=10}size*=magn;if(opts.minTickSize!=null&&size0){if(opts.min==null)axis.min=Math.min(axis.min,niceTicks[0]);if(opts.max==null&&niceTicks.length>1)axis.max=Math.max(axis.max,niceTicks[niceTicks.length-1])}axis.tickGenerator=function(axis){var ticks=[],v,i;for(i=0;i1&&/\..*0$/.test((ts[1]-ts[0]).toFixed(extraDec))))axis.tickDecimals=extraDec}}}}function setTicks(axis){var oticks=axis.options.ticks,ticks=[];if(oticks==null||typeof oticks=="number"&&oticks>0)ticks=axis.tickGenerator(axis);else if(oticks){if($.isFunction(oticks))ticks=oticks(axis);else ticks=oticks}var i,v;axis.ticks=[];for(i=0;i1)label=t[1]}else v=+t;if(label==null)label=axis.tickFormatter(v,axis);if(!isNaN(v))axis.ticks.push({v:v,label:label})}}function snapRangeToTicks(axis,ticks){if(axis.options.autoscaleMargin&&ticks.length>0){if(axis.options.min==null)axis.min=Math.min(axis.min,ticks[0].v);if(axis.options.max==null&&ticks.length>1)axis.max=Math.max(axis.max,ticks[ticks.length-1].v)}}function draw(){surface.clear();executeHooks(hooks.drawBackground,[ctx]);var grid=options.grid;if(grid.show&&grid.backgroundColor)drawBackground();if(grid.show&&!grid.aboveData){drawGrid()}for(var i=0;ito){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function drawBackground(){ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.fillStyle=getColorOrGradient(options.grid.backgroundColor,plotHeight,0,"rgba(255, 255, 255, 0)");ctx.fillRect(0,0,plotWidth,plotHeight);ctx.restore()}function drawGrid(){var i,axes,bw,bc;ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var markings=options.grid.markings;if(markings){if($.isFunction(markings)){axes=plot.getAxes();axes.xmin=axes.xaxis.min;axes.xmax=axes.xaxis.max;axes.ymin=axes.yaxis.min;axes.ymax=axes.yaxis.max;markings=markings(axes)}for(i=0;ixrange.axis.max||yrange.toyrange.axis.max)continue;xrange.from=Math.max(xrange.from,xrange.axis.min);xrange.to=Math.min(xrange.to,xrange.axis.max);yrange.from=Math.max(yrange.from,yrange.axis.min);yrange.to=Math.min(yrange.to,yrange.axis.max);var xequal=xrange.from===xrange.to,yequal=yrange.from===yrange.to;if(xequal&&yequal){continue}xrange.from=Math.floor(xrange.axis.p2c(xrange.from));xrange.to=Math.floor(xrange.axis.p2c(xrange.to));yrange.from=Math.floor(yrange.axis.p2c(yrange.from));yrange.to=Math.floor(yrange.axis.p2c(yrange.to));if(xequal||yequal){var lineWidth=m.lineWidth||options.grid.markingsLineWidth,subPixel=lineWidth%2?.5:0;ctx.beginPath();ctx.strokeStyle=m.color||options.grid.markingsColor;ctx.lineWidth=lineWidth;if(xequal){ctx.moveTo(xrange.to+subPixel,yrange.from);ctx.lineTo(xrange.to+subPixel,yrange.to)}else{ctx.moveTo(xrange.from,yrange.to+subPixel);ctx.lineTo(xrange.to,yrange.to+subPixel)}ctx.stroke()}else{ctx.fillStyle=m.color||options.grid.markingsColor;ctx.fillRect(xrange.from,yrange.to,xrange.to-xrange.from,yrange.from-yrange.to)}}}axes=allAxes();bw=options.grid.borderWidth;for(var j=0;jaxis.max||t=="full"&&(typeof bw=="object"&&bw[axis.position]>0||bw>0)&&(v==axis.min||v==axis.max))continue;if(axis.direction=="x"){x=axis.p2c(v);yoff=t=="full"?-plotHeight:t;if(axis.position=="top")yoff=-yoff}else{y=axis.p2c(v);xoff=t=="full"?-plotWidth:t;if(axis.position=="left")xoff=-xoff}if(ctx.lineWidth==1){if(axis.direction=="x")x=Math.floor(x)+.5;else y=Math.floor(y)+.5}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff)}ctx.stroke()}if(bw){bc=options.grid.borderColor;if(typeof bw=="object"||typeof bc=="object"){if(typeof bw!=="object"){bw={top:bw,right:bw,bottom:bw,left:bw}}if(typeof bc!=="object"){bc={top:bc,right:bc,bottom:bc,left:bc}}if(bw.top>0){ctx.strokeStyle=bc.top;ctx.lineWidth=bw.top;ctx.beginPath();ctx.moveTo(0-bw.left,0-bw.top/2);ctx.lineTo(plotWidth,0-bw.top/2);ctx.stroke()}if(bw.right>0){ctx.strokeStyle=bc.right;ctx.lineWidth=bw.right;ctx.beginPath();ctx.moveTo(plotWidth+bw.right/2,0-bw.top);ctx.lineTo(plotWidth+bw.right/2,plotHeight);ctx.stroke()}if(bw.bottom>0){ctx.strokeStyle=bc.bottom;ctx.lineWidth=bw.bottom;ctx.beginPath();ctx.moveTo(plotWidth+bw.right,plotHeight+bw.bottom/2);ctx.lineTo(0,plotHeight+bw.bottom/2);ctx.stroke()}if(bw.left>0){ctx.strokeStyle=bc.left;ctx.lineWidth=bw.left;ctx.beginPath();ctx.moveTo(0-bw.left/2,plotHeight+bw.bottom);ctx.lineTo(0-bw.left/2,0);ctx.stroke()}}else{ctx.lineWidth=bw;ctx.strokeStyle=options.grid.borderColor;ctx.strokeRect(-bw/2,-bw/2,plotWidth+bw,plotHeight+bw)}}ctx.restore()}function drawAxisLabels(){$.each(allAxes(),function(_,axis){var box=axis.box,legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=axis.options.font||"flot-tick-label tickLabel",tick,x,y,halign,valign;surface.removeText(layer);if(!axis.show||axis.ticks.length==0)return;for(var i=0;iaxis.max)continue;if(axis.direction=="x"){halign="center";x=plotOffset.left+axis.p2c(tick.v);if(axis.position=="bottom"){y=box.top+box.padding}else{y=box.top+box.height-box.padding;valign="bottom"}}else{valign="middle";y=plotOffset.top+axis.p2c(tick.v);if(axis.position=="left"){x=box.left+box.width-box.padding;halign="right"}else{x=box.left+box.padding}}surface.addText(layer,x,y,tick.label,font,null,null,halign,valign)}})}function drawSeries(series){if(series.lines.show)drawSeriesLines(series);if(series.bars.show)drawSeriesBars(series);if(series.points.show)drawSeriesPoints(series)}function drawSeriesLines(series){function plotLine(datapoints,xoffset,yoffset,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,prevx=null,prevy=null;ctx.beginPath();for(var i=ps;i=y2&&y1>axisy.max){if(y2>axisy.max)continue;x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max){if(y1>axisy.max)continue;x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1<=x2&&x1=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(x1!=prevx||y1!=prevy)ctx.moveTo(axisx.p2c(x1)+xoffset,axisy.p2c(y1)+yoffset);prevx=x2;prevy=y2;ctx.lineTo(axisx.p2c(x2)+xoffset,axisy.p2c(y2)+yoffset)}ctx.stroke()}function plotLineArea(datapoints,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,bottom=Math.min(Math.max(0,axisy.min),axisy.max),i=0,top,areaOpen=false,ypos=1,segmentStart=0,segmentEnd=0;while(true){if(ps>0&&i>points.length+ps)break;i+=ps;var x1=points[i-ps],y1=points[i-ps+ypos],x2=points[i],y2=points[i+ypos];if(areaOpen){if(ps>0&&x1!=null&&x2==null){segmentEnd=i;ps=-ps;ypos=2;continue}if(ps<0&&i==segmentStart+ps){ctx.fill();areaOpen=false;ps=-ps;ypos=1;i=segmentStart=segmentEnd+ps;continue}}if(x1==null||x2==null)continue;if(x1<=x2&&x1=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(!areaOpen){ctx.beginPath();ctx.moveTo(axisx.p2c(x1),axisy.p2c(bottom));areaOpen=true}if(y1>=axisy.max&&y2>=axisy.max){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.max));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.max));continue}else if(y1<=axisy.min&&y2<=axisy.min){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.min));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.min));continue}var x1old=x1,x2old=x2;if(y1<=y2&&y1=axisy.min){x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2=axisy.min){x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max&&y2<=axisy.max){x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max&&y1<=axisy.max){x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1!=x1old){ctx.lineTo(axisx.p2c(x1old),axisy.p2c(y1))}ctx.lineTo(axisx.p2c(x1),axisy.p2c(y1));ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));if(x2!=x2old){ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));ctx.lineTo(axisx.p2c(x2old),axisy.p2c(y2))}}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineJoin="round";var lw=series.lines.lineWidth,sw=series.shadowSize;if(lw>0&&sw>0){ctx.lineWidth=sw;ctx.strokeStyle="rgba(0,0,0,0.1)";var angle=Math.PI/18;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/2),Math.cos(angle)*(lw/2+sw/2),series.xaxis,series.yaxis);ctx.lineWidth=sw/2;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/4),Math.cos(angle)*(lw/2+sw/4),series.xaxis,series.yaxis)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;var fillStyle=getFillStyle(series.lines,series.color,0,plotHeight);if(fillStyle){ctx.fillStyle=fillStyle;plotLineArea(series.datapoints,series.xaxis,series.yaxis)}if(lw>0)plotLine(series.datapoints,0,0,series.xaxis,series.yaxis);ctx.restore()}function drawSeriesPoints(series){function plotPoints(datapoints,radius,fillStyle,offset,shadow,axisx,axisy,symbol){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;iaxisx.max||yaxisy.max)continue;ctx.beginPath();x=axisx.p2c(x);y=axisy.p2c(y)+offset;if(symbol=="circle")ctx.arc(x,y,radius,0,shadow?Math.PI:Math.PI*2,false);else symbol(ctx,x,y,radius,shadow);ctx.closePath();if(fillStyle){ctx.fillStyle=fillStyle;ctx.fill()}ctx.stroke()}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var lw=series.points.lineWidth,sw=series.shadowSize,radius=series.points.radius,symbol=series.points.symbol;if(lw==0)lw=1e-4;if(lw>0&&sw>0){var w=sw/2;ctx.lineWidth=w;ctx.strokeStyle="rgba(0,0,0,0.1)";plotPoints(series.datapoints,radius,null,w+w/2,true,series.xaxis,series.yaxis,symbol);ctx.strokeStyle="rgba(0,0,0,0.2)";plotPoints(series.datapoints,radius,null,w/2,true,series.xaxis,series.yaxis,symbol)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;plotPoints(series.datapoints,radius,getFillStyle(series.points,series.color),0,false,series.xaxis,series.yaxis,symbol);ctx.restore()}function drawBar(x,y,b,barLeft,barRight,fillStyleCallback,axisx,axisy,c,horizontal,lineWidth){var left,right,bottom,top,drawLeft,drawRight,drawTop,drawBottom,tmp;if(horizontal){drawBottom=drawRight=drawTop=true;drawLeft=false;left=b;right=x;top=y+barLeft;bottom=y+barRight;if(rightaxisx.max||topaxisy.max)return;if(leftaxisx.max){right=axisx.max;drawRight=false}if(bottomaxisy.max){top=axisy.max;drawTop=false}left=axisx.p2c(left);bottom=axisy.p2c(bottom);right=axisx.p2c(right);top=axisy.p2c(top);if(fillStyleCallback){c.fillStyle=fillStyleCallback(bottom,top);c.fillRect(left,top,right-left,bottom-top)}if(lineWidth>0&&(drawLeft||drawRight||drawTop||drawBottom)){c.beginPath();c.moveTo(left,bottom);if(drawLeft)c.lineTo(left,top);else c.moveTo(left,top);if(drawTop)c.lineTo(right,top);else c.moveTo(right,top);if(drawRight)c.lineTo(right,bottom);else c.moveTo(right,bottom);if(drawBottom)c.lineTo(left,bottom);else c.moveTo(left,bottom);c.stroke()}}function drawSeriesBars(series){function plotBars(datapoints,barLeft,barRight,fillStyleCallback,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i");fragments.push("");rowStarted=true}fragments.push('
'+''+entry.label+"")}if(rowStarted)fragments.push("");if(fragments.length==0)return;var table=''+fragments.join("")+"
";if(options.legend.container!=null)$(options.legend.container).html(table);else{var pos="",p=options.legend.position,m=options.legend.margin;if(m[0]==null)m=[m,m];if(p.charAt(0)=="n")pos+="top:"+(m[1]+plotOffset.top)+"px;";else if(p.charAt(0)=="s")pos+="bottom:"+(m[1]+plotOffset.bottom)+"px;";if(p.charAt(1)=="e")pos+="right:"+(m[0]+plotOffset.right)+"px;";else if(p.charAt(1)=="w")pos+="left:"+(m[0]+plotOffset.left)+"px;";var legend=$('
'+table.replace('style="','style="position:absolute;'+pos+";")+"
").appendTo(placeholder);if(options.legend.backgroundOpacity!=0){var c=options.legend.backgroundColor;if(c==null){c=options.grid.backgroundColor;if(c&&typeof c=="string")c=$.color.parse(c);else c=$.color.extract(legend,"background-color");c.a=1;c=c.toString()}var div=legend.children();$('
').prependTo(legend).css("opacity",options.legend.backgroundOpacity)}}}var highlights=[],redrawTimeout=null;function findNearbyItem(mouseX,mouseY,seriesFilter){var maxDistance=options.grid.mouseActiveRadius,smallestDistance=maxDistance*maxDistance+1,item=null,foundPoint=false,i,j,ps;for(i=series.length-1;i>=0;--i){if(!seriesFilter(series[i]))continue;var s=series[i],axisx=s.xaxis,axisy=s.yaxis,points=s.datapoints.points,mx=axisx.c2p(mouseX),my=axisy.c2p(mouseY),maxx=maxDistance/axisx.scale,maxy=maxDistance/axisy.scale;ps=s.datapoints.pointsize;if(axisx.options.inverseTransform)maxx=Number.MAX_VALUE;if(axisy.options.inverseTransform)maxy=Number.MAX_VALUE;if(s.lines.show||s.points.show){for(j=0;jmaxx||x-mx<-maxx||y-my>maxy||y-my<-maxy)continue;var dx=Math.abs(axisx.p2c(x)-mouseX),dy=Math.abs(axisy.p2c(y)-mouseY),dist=dx*dx+dy*dy;if(dist=Math.min(b,x)&&my>=y+barLeft&&my<=y+barRight:mx>=x+barLeft&&mx<=x+barRight&&my>=Math.min(b,y)&&my<=Math.max(b,y))item=[i,j/ps]}}}if(item){i=item[0];j=item[1];ps=series[i].datapoints.pointsize;return{datapoint:series[i].datapoints.points.slice(j*ps,(j+1)*ps),dataIndex:j,series:series[i],seriesIndex:i}}return null}function onMouseMove(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return s["hoverable"]!=false})}function onMouseLeave(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return false})}function onClick(e){triggerClickHoverEvent("plotclick",e,function(s){return s["clickable"]!=false})}function triggerClickHoverEvent(eventname,event,seriesFilter){var offset=eventHolder.offset(),canvasX=event.pageX-offset.left-plotOffset.left,canvasY=event.pageY-offset.top-plotOffset.top,pos=canvasToAxisCoords({left:canvasX,top:canvasY});pos.pageX=event.pageX;pos.pageY=event.pageY;var item=findNearbyItem(canvasX,canvasY,seriesFilter);if(item){item.pageX=parseInt(item.series.xaxis.p2c(item.datapoint[0])+offset.left+plotOffset.left,10);item.pageY=parseInt(item.series.yaxis.p2c(item.datapoint[1])+offset.top+plotOffset.top,10)}if(options.grid.autoHighlight){for(var i=0;iaxisx.max||yaxisy.max)return;var pointRadius=series.points.radius+series.points.lineWidth/2;octx.lineWidth=pointRadius;octx.strokeStyle=highlightColor;var radius=1.5*pointRadius;x=axisx.p2c(x);y=axisy.p2c(y);octx.beginPath();if(series.points.symbol=="circle")octx.arc(x,y,radius,0,2*Math.PI,false);else series.points.symbol(octx,x,y,radius,false);octx.closePath();octx.stroke()}function drawBarHighlight(series,point){var highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString(),fillStyle=highlightColor,barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}octx.lineWidth=series.bars.lineWidth;octx.strokeStyle=highlightColor;drawBar(point[0],point[1],point[2]||0,barLeft,barLeft+series.bars.barWidth,function(){return fillStyle},series.xaxis,series.yaxis,octx,series.bars.horizontal,series.bars.lineWidth)}function getColorOrGradient(spec,bottom,top,defaultColor){if(typeof spec=="string")return spec;else{var gradient=ctx.createLinearGradient(0,top,0,bottom);for(var i=0,l=spec.colors.length;i