✨ 🚀 🧪 add fit file feature
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
5286bdabfb
commit
15e854a508
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
engines:
|
||||
phpmd:
|
||||
enabled: true
|
||||
|
||||
ratings:
|
||||
paths:
|
||||
- "**.php"
|
||||
|
||||
exclude_paths:
|
||||
- "demo/"
|
||||
- "tests/"
|
@ -0,0 +1,17 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
@ -0,0 +1,49 @@
|
||||
vendor
|
||||
issues
|
||||
composer.phar
|
||||
composer.lock
|
||||
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# =========================
|
||||
# Operating System Files
|
||||
# =========================
|
||||
|
||||
# OSX
|
||||
# =========================
|
||||
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
/nbproject
|
@ -0,0 +1,15 @@
|
||||
dist: trusty
|
||||
language: php
|
||||
php:
|
||||
- 5.5
|
||||
|
||||
before_script:
|
||||
- curl -s http://getcomposer.org/installer | php
|
||||
- php composer.phar install --dev --no-interaction
|
||||
|
||||
script:
|
||||
- mkdir -p build/logs
|
||||
- phpunit --coverage-clover build/logs/clover.xml tests
|
||||
|
||||
after_script:
|
||||
- php vendor/bin/coveralls -v
|
@ -0,0 +1,383 @@
|
||||
[](https://travis-ci.org/adriangibbons/php-fit-file-analysis) [](https://packagist.org/packages/adriangibbons/php-fit-file-analysis) [](https://packagist.org/packages/adriangibbons/php-fit-file-analysis) [](https://coveralls.io/github/adriangibbons/php-fit-file-analysis?branch=master)
|
||||
# phpFITFileAnalysis
|
||||
|
||||
A PHP (>= v5.4) class for analysing FIT files created by Garmin GPS devices.
|
||||
|
||||
[Live demonstration](http://adriangibbons.com/php-fit-file-analysis/demo/) (Right-click and Open in new tab)
|
||||
|
||||
## Demo Screenshots
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
## What is a FIT file?
|
||||
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.
|
||||
|
||||
Visit the FAQ page within the Wiki for more information.
|
||||
|
||||
## How do I use phpFITFileAnalysis with my PHP-driven website?
|
||||
|
||||
A couple of choices here:
|
||||
|
||||
**The more modern way:** Add the package *adriangibbons/php-fit-file-analysis* in a composer.json file:
|
||||
```JSON
|
||||
{
|
||||
"require": {
|
||||
"adriangibbons/php-fit-file-analysis": "^3.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
Run ```composer update``` from the command line.
|
||||
|
||||
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:
|
||||
```php
|
||||
<?php
|
||||
require __DIR__ . '/vendor/autoload.php'; // this file is in the project's root folder
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis('fit_files/my_fit_file.fit');
|
||||
?>
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
Then include the file on the PHP page where you want to use it and instantiate an object of the class:
|
||||
```php
|
||||
<?php
|
||||
include('classes/phpFITFileAnalysis.php');
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis('fit_files/my_fit_file.fit');
|
||||
?>
|
||||
```
|
||||
Note that the only mandatory parameter required when creating an instance is the path to the FIT file that you want to load.
|
||||
|
||||
There are more **Optional Parameters** that can be supplied. These are described in more detail further down this page.
|
||||
|
||||
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.
|
||||
|
||||
### Accessing the Data
|
||||
Data read by the class are stored in associative arrays, which are accessible via the public data variable:
|
||||
```php
|
||||
$pFFA->data_mesgs
|
||||
```
|
||||
The array indexes are the names of the messages and fields that they contain. For example:
|
||||
```php
|
||||
// Contains an array of all heart_rate data read from the file, indexed by timestamp
|
||||
$pFFA->data_mesgs['record']['heart_rate']
|
||||
// Contains an integer identifying the number of laps
|
||||
$pFFA->data_mesgs['session']['num_laps']
|
||||
```
|
||||
**OK, but how do I know what messages and fields are in my file?**
|
||||
You could either iterate through the $pFFA->data_mesgs array, or take a look at the debug information you can dump to a webpage:
|
||||
```php
|
||||
// Option 1. Iterate through the $pFFA->data_mesgs array
|
||||
foreach ($pFFA->data_mesgs as $mesg_key => $mesg) { // Iterate the array and output the messages
|
||||
echo "<strong>Found Message: $mesg_key</strong><br>";
|
||||
foreach ($mesg as $field_key => $field) { // Iterate each message and output the fields
|
||||
echo " - Found Field: $mesg_key -> $field_key<br>";
|
||||
}
|
||||
echo "<br>";
|
||||
}
|
||||
|
||||
// Option 2. Show the debug information
|
||||
$pFFA->showDebugInfo(); // Quite a lot of info...
|
||||
```
|
||||
**How about some real-world examples?**
|
||||
```php
|
||||
// Get Max and Avg Speed
|
||||
echo "Maximum Speed: ".max($pFFA->data_mesgs['record']['speed'])."<br>";
|
||||
echo "Average Speed: ".( array_sum($pFFA->data_mesgs['record']['speed']) / count($pFFA->data_mesgs['record']['speed']) )."<br>";
|
||||
|
||||
// Put HR data into a JavaScript array for use in a Chart
|
||||
echo "var chartData = [";
|
||||
foreach ($pFFA->data_mesgs['record']['heart_rate'] as $timestamp => $hr_value) {
|
||||
echo "[$timestamp,$hr_value],";
|
||||
}
|
||||
echo "];";
|
||||
```
|
||||
**Enumerated Data**
|
||||
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.
|
||||
|
||||
A public function is available, which will return the enumerated value for a given message type. For example:
|
||||
```php
|
||||
// Access data stored within the private class variable $enum_data
|
||||
// $pFFA->enumData($type, $value)
|
||||
// e.g.
|
||||
echo $pFFA->enumData('sport', 2)); // returns 'cycling'
|
||||
echo $pFFA->enumData('manufacturer', $this->data_mesgs['device_info']['manufacturer']); // returns 'Garmin';
|
||||
echo $pFFA->manufacturer(); // Short-hand for above
|
||||
```
|
||||
In addition, public functions provide a short-hand way to access commonly used enumerated data:
|
||||
|
||||
- manufacturer()
|
||||
- product()
|
||||
- sport()
|
||||
|
||||
### Optional Parameters
|
||||
There are five optional parameters that can be passed as an associative array when the phpFITFileAnalysis object is instantiated. These are:
|
||||
|
||||
- fix_data
|
||||
- data_every_second
|
||||
- units
|
||||
- pace
|
||||
- garmin_timestamps
|
||||
- overwrite_with_dev_data
|
||||
|
||||
For example:
|
||||
```php
|
||||
$options = [
|
||||
'fix_data' => ['cadence', 'distance'],
|
||||
'data_every_second' => true
|
||||
'units' => 'statute',
|
||||
'pace' => true,
|
||||
'garmin_timestamps' => true,
|
||||
'overwrite_with_dev_data' => false
|
||||
];
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis('my_fit_file.fit', $options);
|
||||
```
|
||||
The optional parameters are described in more detail below.
|
||||
#### "Fix" the Data
|
||||
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.
|
||||
|
||||
**Recognised values:** 'all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'power', 'speed'
|
||||
|
||||
**Examples: **
|
||||
```php
|
||||
$options = ['fix_data' => ['all']]; // fix cadence, distance, heart_rate, lat_lon, power, and speed data
|
||||
$options = ['fix_data' => ['cadence', 'distance']]; // fix cadence and distance data only
|
||||
$options = ['fix_data' => ['lat_lon']]; // fix position data only
|
||||
```
|
||||
If the *fix_data* array is not supplied, then no "fixing" of the data is performed.
|
||||
|
||||
A FIT file might contain the following:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th></th>
|
||||
<th># Data Points</th>
|
||||
<th>Delta (c.f. Timestamps)</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>timestamp</td><td>10251</td><td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>position_lat</td><td>10236</td><td>25</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>position_long</td><td>10236</td><td>25</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>altitude</td><td>10251</td><td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>heart_rate</td><td>10251</td><td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cadence</td><td>9716</td><td>535</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>distance</td><td>10236</td><td>25</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>speed</td><td>10236</td><td>25</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>power</td><td>10242</td><td>9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>temperature</td><td>10251</td><td>0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
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.
|
||||
|
||||
With the exception of cadence information, missing data points are "fixed" by inserting interpolated values.
|
||||
|
||||
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.
|
||||
|
||||
**Interpolation of missing data points**
|
||||
```php
|
||||
// Do not use code, just for demonstration purposes
|
||||
var_dump($pFFA->data_mesgs['record']['temperature']); // ['100'=>22, '101'=>22, '102'=>23, '103'=>23, '104'=>23];
|
||||
var_dump($pFFA->data_mesgs['record']['distance']); // ['100'=>3.62, '101'=>4.01, '104'=>10.88];
|
||||
```
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
The result would be:
|
||||
```php
|
||||
var_dump($pFFA->data_mesgs['record']['distance']); // ['100'=>3.62, '101'=>4.01, '102'=>6.30, '103'=>8.59, '104'=>10.88];
|
||||
```
|
||||
|
||||
#### Data Every Second
|
||||
Some of Garmin's Fitness devices offer the choice of Smart Recording or Every Second Recording.
|
||||
|
||||
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.
|
||||
|
||||
You can force timestamps to be regular one second intervals by setting the option:
|
||||
```php
|
||||
$options = ['data_every_second' => true];
|
||||
```
|
||||
Missing timestamps will have data interpolated as per the ```fix_data``` option above.
|
||||
|
||||
If the ```fix_data``` option is not specified in conjunction with ```data_every_second``` then ```'fix_data' => ['all']``` is assumed.
|
||||
|
||||
*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.*
|
||||
|
||||
#### Set Units
|
||||
By default, **metric** units (identified in the table below) are assumed.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Metric<br><em>(DEFAULT)</em></th>
|
||||
<th>Statute</th>
|
||||
<th>Raw</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Speed</td><td>kilometers per hour</td><td>miles per hour</td><td>meters per second</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Distance</td><td>kilometers</td><td>miles</td><td>meters</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Altitude</td><td>meters</td><td>feet</td><td>meters</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latitude</td><td>degrees</td><td>degrees</td><td>semicircles</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Longitude</td><td>degrees</td><td>degrees</td><td>semicircles</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Temperature</td><td>celsius (℃)</td><td>fahrenheit (℉)</td><td>celsius (℃)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
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).
|
||||
|
||||
To select the units you require, use one of the following:
|
||||
```php
|
||||
$options = ['units' => 'statute'];
|
||||
$options = ['units' => 'raw'];
|
||||
$options = ['units' => 'metric']; // explicit but not necessary, same as default
|
||||
```
|
||||
#### Pace
|
||||
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.
|
||||
|
||||
To select pace, use the following option:
|
||||
```php
|
||||
$options = ['pace' => true];
|
||||
```
|
||||
Pace values will be decimal minutes. To get the seconds, you may wish to do something like:
|
||||
```php
|
||||
foreach ($pFFA->data_mesgs['record']['speed'] as $key => $value) {
|
||||
$min = floor($value);
|
||||
$sec = round(60 * ($value - $min));
|
||||
echo "pace: $min min $sec sec<br>";
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
#### Timestamps
|
||||
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**.
|
||||
|
||||
The difference (in seconds) between FIT and Unix timestamps is 631,065,600:
|
||||
```php
|
||||
$date_FIT = new DateTime('1989-12-31 00:00:00', new DateTimeZone('UTC'));
|
||||
$date_UNIX = new DateTime('1970-01-01 00:00:00', new DateTimeZone('UTC'));
|
||||
$diff = $date_FIT->getTimestamp() - $date_UNIX->getTimestamp();
|
||||
echo 'The difference (in seconds) between FIT and Unix timestamps is '. number_format($diff);
|
||||
```
|
||||
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.
|
||||
|
||||
#### Overwrite with Developer Data
|
||||
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']```.
|
||||
|
||||
## Analysis
|
||||
The following functions return arrays of data that could be used to create tables/charts:
|
||||
```php
|
||||
array $pFFA->hrPartionedHRmaximum(int $hr_maximum);
|
||||
array $pFFA->hrPartionedHRreserve(int $hr_resting, int $hr_maximum);
|
||||
array $pFFA->powerPartioned(int $functional_threshold_power);
|
||||
array $pFFA->powerHistogram(int $bucket_width = 25);
|
||||
```
|
||||
For advanced control over these functions, or use with other sensor data (e.g. cadence or speed), use the underlying functions:
|
||||
```php
|
||||
array $pFFA->partitionData(string $record_field='', $thresholds=null, bool $percentages = true, bool $labels_for_keys = true);
|
||||
array $pFFA->histogram(int $bucket_width=25, string $record_field='');
|
||||
```
|
||||
Functions exist to determine thresholds based on percentages of user-supplied data:
|
||||
```php
|
||||
array $pFFA->hrZonesMax(int $hr_maximum, array $percentages_array=[0.60, 0.75, 0.85, 0.95]);
|
||||
array $pFFA->hrZonesReserve(int $hr_resting, int $hr_maximum, array $percentages_array=[0.60, 0.65, 0.75, 0.82, 0.89, 0.94 ]) {
|
||||
array $pFFA->powerZones(int $functional_threshold_power, array $percentages_array=[0.55, 0.75, 0.90, 1.05, 1.20, 1.50]);
|
||||
```
|
||||
### Heart Rate
|
||||
A function exists for analysing heart rate data:
|
||||
```php
|
||||
// hr_FT is heart rate at Functional Threshold, or Lactate Threshold Heart Rate
|
||||
array $pFFA->hrMetrics(int $hr_resting, int $hr_maximum, string $hr_FT, $gender);
|
||||
// e.g. $pFFA->hrMetrics(52, 189, 172, 'male');
|
||||
```
|
||||
**Heart Rate metrics:**
|
||||
* TRIMP (TRaining IMPulse)
|
||||
* Intensity Factor
|
||||
|
||||
### Power
|
||||
Three functions exist for analysing power data:
|
||||
```php
|
||||
array $pFFA->powerMetrics(int $functional_threshold_power);
|
||||
array $pFFA->criticalPower(int or array $time_periods); // e.g. 300 or [600, 900]
|
||||
array $pFFA->quadrantAnalysis(float $crank_length, int $ftp, int $selected_cadence = 90, bool $use_timestamps = false); // Crank length in metres
|
||||
```
|
||||
**Power metrics:**
|
||||
* Average Power
|
||||
* Kilojoules
|
||||
* Normalised Power (estimate had your power output been constant)
|
||||
* Variability Index (ratio of Normalised Power / Average Power)
|
||||
* Intensity Factor (ratio of Normalised Power / Functional Threshold Power)
|
||||
* Training Stress Score (effort based on relative intensity and duration)
|
||||
|
||||
**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.
|
||||
|
||||
**Quadrant Analysis** provides insight into the neuromuscular demands of a bike ride through comparing pedal velocity with force by looking at cadence and power.
|
||||
|
||||
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!
|
||||
|
||||
A demo of power analysis is available [here](http://adriangibbons.com/php-fit-file-analysis/demo/power-analysis.php).
|
||||
|
||||
## Other methods
|
||||
Returns array of booleans using timestamp as key. true == timer paused (e.g. autopause):
|
||||
```php
|
||||
array isPaused()
|
||||
```
|
||||
Returns a JSON object with requested ride data:
|
||||
```php
|
||||
array getJSON(float $crank_length = null, int $ftp = null, array $data_required = ['all'], int $selected_cadence = 90)
|
||||
/**
|
||||
* $data_required can be ['all'] or a combination of:
|
||||
* ['timestamp', 'paused', 'temperature', 'lap', 'position_lat', 'position_long', 'distance', 'altitude', 'speed', 'heart_rate', 'cadence', 'power', 'quadrant-analysis']
|
||||
*/
|
||||
```
|
||||
Returns array of gear change information (if present, e.g. using Shimano D-Fly Wireless Di2 Transmitter):
|
||||
```php
|
||||
// By default, time spent in a gear whilst the timer is paused (e.g. autopause) is ignored. Set to false to include.
|
||||
array gearChanges($bIgnoreTimerPaused = true)
|
||||
```
|
||||
|
||||
## Acknowledgement
|
||||
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)).
|
||||
|
||||
As a minimum, I'd recommend reading the three PDFs included in the SDK:
|
||||
|
||||
1. FIT File Types Description
|
||||
2. FIT SDK Introductory Guide
|
||||
3. Flexible & Interoperable Data Transfer (FIT) Protocol
|
||||
|
||||
Following these, the 'Profile.xls' spreadsheet and then the Java/C/C++ examples.
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "adriangibbons/php-fit-file-analysis",
|
||||
"type": "library",
|
||||
"description": "A PHP class for analysing FIT files created by Garmin GPS devices",
|
||||
"keywords": ["garmin", "fit"],
|
||||
"homepage": "https://github.com/adriangibbons/php-fit-file-analysis",
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.8.*",
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"satooshi/php-coveralls": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"adriangibbons\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,319 @@
|
||||
#lap-row-chart svg g g.axis { display: none; }
|
||||
|
||||
div.dc-chart {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.dc-chart rect.bar {
|
||||
stroke: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-chart rect.bar:hover {
|
||||
fill-opacity: .5;
|
||||
}
|
||||
|
||||
.dc-chart rect.stack1 {
|
||||
stroke: none;
|
||||
fill: red;
|
||||
}
|
||||
|
||||
.dc-chart rect.stack2 {
|
||||
stroke: none;
|
||||
fill: green;
|
||||
}
|
||||
|
||||
.dc-chart rect.deselected {
|
||||
stroke: none;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.dc-chart .empty-chart .pie-slice path {
|
||||
fill: #FFEEEE;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dc-chart .empty-chart .pie-slice {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dc-chart .pie-slice {
|
||||
fill: white;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-chart .pie-slice.external{
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.dc-chart .pie-slice :hover {
|
||||
fill-opacity: .8;
|
||||
}
|
||||
|
||||
.dc-chart .pie-slice.highlight {
|
||||
fill-opacity: .8;
|
||||
}
|
||||
|
||||
.dc-chart .selected path {
|
||||
stroke-width: 3;
|
||||
stroke: #ccc;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.dc-chart .deselected path {
|
||||
stroke: none;
|
||||
fill-opacity: .5;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.dc-chart .axis path, .axis line {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.dc-chart .axis text {
|
||||
font: 10px sans-serif;
|
||||
}
|
||||
|
||||
.dc-chart .grid-line {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
opacity: .5;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.dc-chart .grid-line line {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
opacity: .5;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.dc-chart .brush rect.background {
|
||||
z-index: -999;
|
||||
}
|
||||
|
||||
.dc-chart .brush rect.extent {
|
||||
fill: steelblue;
|
||||
fill-opacity: .125;
|
||||
}
|
||||
|
||||
.dc-chart .brush .resize path {
|
||||
fill: #eee;
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
.dc-chart path.line {
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.dc-chart circle.dot {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.dc-chart g.dc-tooltip path {
|
||||
fill: none;
|
||||
stroke: grey;
|
||||
stroke-opacity: .8;
|
||||
}
|
||||
|
||||
.dc-chart path.area {
|
||||
fill-opacity: .3;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.dc-chart .node {
|
||||
font-size: 0.7em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-chart .node :hover {
|
||||
fill-opacity: .8;
|
||||
}
|
||||
|
||||
.dc-chart .selected circle {
|
||||
stroke-width: 3;
|
||||
stroke: #ccc;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.dc-chart .deselected circle {
|
||||
stroke: none;
|
||||
fill-opacity: .5;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.dc-chart .bubble {
|
||||
stroke: none;
|
||||
fill-opacity: 0.6;
|
||||
}
|
||||
|
||||
.dc-data-count {
|
||||
float: right;
|
||||
margin-top: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.dc-data-count .filter-count {
|
||||
color: #3182bd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dc-data-count .total-count {
|
||||
color: #3182bd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dc-data-table {
|
||||
}
|
||||
|
||||
.dc-chart g.state {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-chart g.state :hover {
|
||||
fill-opacity: .8;
|
||||
}
|
||||
|
||||
.dc-chart g.state path {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.dc-chart g.selected path {
|
||||
}
|
||||
|
||||
.dc-chart g.deselected path {
|
||||
fill: grey;
|
||||
}
|
||||
|
||||
.dc-chart g.selected text {
|
||||
}
|
||||
|
||||
.dc-chart g.deselected text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dc-chart g.county path {
|
||||
stroke: white;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.dc-chart g.debug rect {
|
||||
fill: blue;
|
||||
fill-opacity: .2;
|
||||
}
|
||||
|
||||
.dc-chart g.row rect {
|
||||
fill-opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-chart g.row rect:hover {
|
||||
fill-opacity: 0.6;
|
||||
}
|
||||
|
||||
.dc-chart g.row text {
|
||||
fill: #333;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-legend {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dc-legend-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dc-chart g.axis text {
|
||||
/* Makes it so the user can't accidentally click and select text that is meant as a label only */
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10 */
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dc-chart path.highlight {
|
||||
stroke-width: 3;
|
||||
fill-opacity: 1;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.dc-chart .highlight {
|
||||
fill-opacity: 1;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.dc-chart .fadeout {
|
||||
fill-opacity: 0.2;
|
||||
stroke-opacity: 0.2;
|
||||
}
|
||||
|
||||
.dc-chart path.dc-symbol, g.dc-legend-item.fadeout {
|
||||
fill-opacity: 0.5;
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
|
||||
.dc-hard .number-display {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.dc-chart .box text {
|
||||
font: 10px sans-serif;
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10 */
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dc-chart .box line,
|
||||
.dc-chart .box circle {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.dc-chart .box rect {
|
||||
stroke: #000;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.dc-chart .box .center {
|
||||
stroke-dasharray: 3,3;
|
||||
}
|
||||
|
||||
.dc-chart .box .outlier {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
}
|
||||
|
||||
.dc-chart .box.deselected .box {
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.dc-chart .box.deselected {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.dc-chart .symbol{
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.dc-chart .heatmap .box-group.deselected rect {
|
||||
stroke: none;
|
||||
fill-opacity: .5;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.dc-chart .heatmap g.axis text {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 28 KiB |
@ -0,0 +1,61 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>phpFITFileAnalysis demo</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
|
||||
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Mountain Biking</h3>
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
<a href="mountain-biking.php"><img src="img/mountain-biking.jpg" alt="Mountain Biking"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Power Analysis <small>(cycling)</small></h3>
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
<a href="power-analysis.php"><img src="img/power-analysis.jpg" alt="Power Analysis"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Quadrant Analysis</h3>
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
<a href="quadrant-analysis.php"><img src="img/quadrant-analysis.jpg" alt="Quadrant Analysis"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> Swim</h3>
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
<a href="swim.php"><img src="img/swim.jpg" alt="Swim"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/*
|
||||
* A Ramer-Douglas-Peucker implementation to simplify lines in PHP
|
||||
* Unlike the one by Pol Dell'Aiera this one is built to operate on an array of arrays and in a non-OO manner,
|
||||
* making it suitable for smaller apps which must consume input from ArcGIS or Leaflet, without the luxury of GeoPHP/GEOS
|
||||
*
|
||||
* Usage:
|
||||
* $verts = array( array(0,1), array(1,2), array(2,1), array(3,5), array(4,6), array(5,5) );
|
||||
* $tolerance = 1.0;
|
||||
* $newverts = simplify_RDP($verts,$tolerance);
|
||||
*
|
||||
* Bonus: It does not trim off extra ordinates from each vertex, so it's agnostic as to whether your data are 2D or 3D
|
||||
* and will return the kept vertices unchanged.
|
||||
*
|
||||
* This operates on a single set of vertices, aka a single linestring.
|
||||
* If used on a multilinestring you will want to run it on each component linestring separately.
|
||||
*
|
||||
* No license, use as you will, credits appreciated but not required, etc.
|
||||
* Greg Allensworth, GreenInfo Network <gregor@greeninfo.org>
|
||||
*
|
||||
* My invaluable references:
|
||||
* https://github.com/Polzme/geoPHP/commit/56c9072f69ed1cec2fdd36da76fa595792b4aa24
|
||||
* http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
|
||||
* http://math.ucsd.edu/~wgarner/math4c/derivations/distance/distptline.htm
|
||||
*/
|
||||
|
||||
function simplify_RDP($vertices, $tolerance) {
|
||||
// if this is a multilinestring, then we call ourselves one each segment individually, collect the list, and return that list of simplified lists
|
||||
if (is_array($vertices[0][0])) {
|
||||
$multi = array();
|
||||
foreach ($vertices as $subvertices) $multi[] = simplify_RDP($subvertices,$tolerance);
|
||||
return $multi;
|
||||
}
|
||||
|
||||
$tolerance2 = $tolerance * $tolerance;
|
||||
|
||||
// okay, so this is a single linestring and we simplify it individually
|
||||
return _segment_RDP($vertices,$tolerance2);
|
||||
}
|
||||
|
||||
function _segment_RDP($segment, $tolerance_squared) {
|
||||
if (sizeof($segment) <= 2) return $segment; // segment is too small to simplify, hand it back as-is
|
||||
|
||||
// find the maximum distance (squared) between this line $segment and each vertex
|
||||
// distance is solved as described at UCSD page linked above
|
||||
// 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
|
||||
$startx = (float) $segment[0][0];
|
||||
$starty = (float) $segment[0][1];
|
||||
$endx = (float) $segment[ sizeof($segment)-1 ][0];
|
||||
$endy = (float) $segment[ sizeof($segment)-1 ][1];
|
||||
if ($endx == $startx) $startx += 0.00001;
|
||||
$m = ($endy - $starty) / ($endx - $startx); // slope, as in y = mx + b
|
||||
$b = $starty - ($m * $startx); // y-intercept, as in y = mx + b
|
||||
|
||||
$max_distance_squared = 0;
|
||||
$max_distance_index = null;
|
||||
for ($i=1, $l=sizeof($segment); $i<=$l-2; $i++) {
|
||||
$x1 = $segment[$i][0];
|
||||
$y1 = $segment[$i][1];
|
||||
|
||||
$closestx = ( ($m*$y1) + ($x1) - ($m*$b) ) / ( ($m*$m)+1);
|
||||
$closesty = ($m * $closestx) + $b;
|
||||
$distsqr = ($closestx-$x1)*($closestx-$x1) + ($closesty-$y1)*($closesty-$y1);
|
||||
|
||||
if ($distsqr > $max_distance_squared) {
|
||||
$max_distance_squared = $distsqr;
|
||||
$max_distance_index = $i;
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup and disposition
|
||||
// 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)
|
||||
if ($max_distance_squared <= $tolerance_squared) {
|
||||
return array($segment[0], $segment[ sizeof($segment)-1 ]);
|
||||
}
|
||||
// but if we got here then a vertex falls outside the tolerance
|
||||
// split the line segment into two smaller segments at that "maximum error vertex" and simplify those
|
||||
$slice1 = array_slice($segment, 0, $max_distance_index);
|
||||
$slice2 = array_slice($segment, $max_distance_index);
|
||||
$segs1 = _segment_RDP($slice1, $tolerance_squared);
|
||||
$segs2 = _segment_RDP($slice2, $tolerance_squared);
|
||||
return array_merge($segs1,$segs2);
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* PHP Polyline Encoder
|
||||
*
|
||||
*/
|
||||
|
||||
class PolylineEncoder {
|
||||
|
||||
private $points;
|
||||
private $encoded;
|
||||
|
||||
public function __construct() {
|
||||
$this->points = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a point
|
||||
*
|
||||
* @param float $lat : lattitude
|
||||
* @param float $lng : longitude
|
||||
*/
|
||||
function addPoint($lat, $lng) {
|
||||
if (empty($this->points)) {
|
||||
$this->points[] = array('x' => $lat, 'y' => $lng);
|
||||
$this->encoded = $this->encodeValue($lat) . $this->encodeValue($lng);
|
||||
} else {
|
||||
$n = count($this->points);
|
||||
$prev_p = $this->points[$n-1];
|
||||
$this->points[] = array('x' => $lat, 'y' => $lng);
|
||||
$this->encoded .= $this->encodeValue($lat-$prev_p['x']) . $this->encodeValue($lng-$prev_p['y']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the encoded string generated from the points
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function encodedString() {
|
||||
return $this->encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a value following Google Maps API v3 algorithm
|
||||
*
|
||||
* @param type $value
|
||||
* @return type
|
||||
*/
|
||||
function encodeValue($value) {
|
||||
$encoded = "";
|
||||
$value = round($value * 100000);
|
||||
$r = ($value < 0) ? ~($value << 1) : ($value << 1);
|
||||
|
||||
while ($r >= 0x20) {
|
||||
$val = (0x20|($r & 0x1f)) + 63;
|
||||
$encoded .= chr($val);
|
||||
$r >>= 5;
|
||||
}
|
||||
$lastVal = $r + 63;
|
||||
$encoded .= chr($lastVal);
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an encoded polyline string to an array of points
|
||||
*
|
||||
* @param type $value
|
||||
* @return type
|
||||
*/
|
||||
static public function decodeValue($value) {
|
||||
$index = 0;
|
||||
$points = array();
|
||||
$lat = 0;
|
||||
$lng = 0;
|
||||
|
||||
while ($index < strlen($value)) {
|
||||
$b;
|
||||
$shift = 0;
|
||||
$result = 0;
|
||||
do {
|
||||
$b = ord(substr($value, $index++, 1)) - 63;
|
||||
$result |= ($b & 0x1f) << $shift;
|
||||
$shift += 5;
|
||||
} while ($b > 31);
|
||||
$dlat = (($result & 1) ? ~($result >> 1) : ($result >> 1));
|
||||
$lat += $dlat;
|
||||
|
||||
$shift = 0;
|
||||
$result = 0;
|
||||
do {
|
||||
$b = ord(substr($value, $index++, 1)) - 63;
|
||||
$result |= ($b & 0x1f) << $shift;
|
||||
$shift += 5;
|
||||
} while ($b > 31);
|
||||
$dlng = (($result & 1) ? ~($result >> 1) : ($result >> 1));
|
||||
$lng += $dlng;
|
||||
|
||||
$points[] = array('x' => $lat/100000, 'y' => $lng/100000);
|
||||
}
|
||||
|
||||
return $points;
|
||||
}
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
<?php
|
||||
/**
|
||||
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
|
||||
* https://github.com/adriangibbons/phpFITFileAnalysis
|
||||
*
|
||||
* Not intended to be demonstration of how to best use Google APIs, but works for me!
|
||||
*
|
||||
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
|
||||
*/
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
require __DIR__ . '/libraries/PolylineEncoder.php'; // https://github.com/dyaaj/polyline-encoder
|
||||
require __DIR__ . '/libraries/Line_DouglasPeucker.php'; // https://github.com/gregallensworth/PHP-Geometry
|
||||
|
||||
try {
|
||||
$file = '/fit_files/mountain-biking.fit';
|
||||
|
||||
$options = [
|
||||
// Just using the defaults so no need to provide
|
||||
// 'fix_data' => [],
|
||||
// 'units' => 'metric',
|
||||
// 'pace' => false
|
||||
];
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
|
||||
} catch (Exception $e) {
|
||||
echo 'caught exception: '.$e->getMessage();
|
||||
die();
|
||||
}
|
||||
|
||||
// Google Static Maps API
|
||||
$position_lat = $pFFA->data_mesgs['record']['position_lat'];
|
||||
$position_long = $pFFA->data_mesgs['record']['position_long'];
|
||||
$lat_long_combined = [];
|
||||
|
||||
foreach ($position_lat as $key => $value) { // Assumes every lat has a corresponding long
|
||||
$lat_long_combined[] = [$position_lat[$key],$position_long[$key]];
|
||||
}
|
||||
|
||||
$delta = 0.0001;
|
||||
do {
|
||||
$RDP_LatLng_coord = simplify_RDP($lat_long_combined, $delta); // Simplify the array of coordinates using the Ramer-Douglas-Peucker algorithm.
|
||||
$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
|
||||
|
||||
$polylineEncoder = new PolylineEncoder(); // Create an encoded string to pass as the path variable for the Google Static Maps API
|
||||
foreach ($RDP_LatLng_coord as $RDP) {
|
||||
$polylineEncoder->addPoint($RDP[0], $RDP[1]);
|
||||
}
|
||||
$map_encoded_polyline = $polylineEncoder->encodedString();
|
||||
|
||||
$map_string = '&path=color:red%7Cenc:'.$map_encoded_polyline;
|
||||
} while (strlen($map_string) > 1800); // Google Map web service URL limit is 2048 characters. 1800 is arbitrary attempt to stay under 2048
|
||||
|
||||
$LatLng_start = implode(',', $lat_long_combined[0]);
|
||||
$LatLng_finish = implode(',', $lat_long_combined[count($lat_long_combined)-1]);
|
||||
|
||||
$map_string .= '&markers=color:red%7Clabel:F%7C'.$LatLng_finish.'&markers=color:green%7Clabel:S%7C'.$LatLng_start;
|
||||
|
||||
|
||||
// Google Time Zone API
|
||||
$date = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$date_s = $pFFA->data_mesgs['session']['start_time'];
|
||||
|
||||
$url_tz = 'https://maps.googleapis.com/maps/api/timezone/json?location='.$LatLng_start.'×tamp='.$date_s.'&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE';
|
||||
|
||||
$result = file_get_contents($url_tz);
|
||||
$json_tz = json_decode($result);
|
||||
if ($json_tz->status == 'OK') {
|
||||
$date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset;
|
||||
} else {
|
||||
$json_tz->timeZoneName = 'Error';
|
||||
}
|
||||
$date->setTimestamp($date_s);
|
||||
|
||||
|
||||
// Google Geocoding API
|
||||
$location = 'Error';
|
||||
$url_coord = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='.$LatLng_start.'&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE';
|
||||
$result = file_get_contents($url_coord);
|
||||
$json_coord = json_decode($result);
|
||||
if ($json_coord->status == 'OK') {
|
||||
foreach ($json_coord->results[0]->address_components as $addressPart) {
|
||||
if ((in_array('locality', $addressPart->types)) && (in_array('political', $addressPart->types))) {
|
||||
$city = $addressPart->long_name;
|
||||
} elseif ((in_array('administrative_area_level_1', $addressPart->types)) && (in_array('political', $addressPart->types))) {
|
||||
$state = $addressPart->short_name;
|
||||
} elseif ((in_array('country', $addressPart->types)) && (in_array('political', $addressPart->types))) {
|
||||
$country = $addressPart->long_name;
|
||||
}
|
||||
}
|
||||
$location = $city.', '.$state.', '.$country;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>phpFITFileAnalysis demo</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
|
||||
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>File: </dt>
|
||||
<dd><?php echo $file; ?></dd>
|
||||
<dt>Device: </dt>
|
||||
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
|
||||
<dt>Sport: </dt>
|
||||
<dd><?php echo $pFFA->sport(); ?></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Recorded: </dt>
|
||||
<dd>
|
||||
<?php
|
||||
echo $date->format('D, d-M-y @ g:ia');
|
||||
?>
|
||||
</dd>
|
||||
<dt>Duration: </dt>
|
||||
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
|
||||
<dt>Distance: </dt>
|
||||
<dd><?php echo max($pFFA->data_mesgs['record']['distance']); ?> km</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Messages</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<?php
|
||||
// Output all the Messages read in the FIT file.
|
||||
foreach ($pFFA->data_mesgs as $mesg_key => $mesg) {
|
||||
if ($mesg_key == 'record') {
|
||||
echo '<strong><mark><u>';
|
||||
}
|
||||
echo $mesg_key.'<br>';
|
||||
if ($mesg_key == 'record') {
|
||||
echo '</u></mark></strong>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Record Fields</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<?php
|
||||
// Output all the Fields found in Record messages within the FIT file.
|
||||
foreach ($pFFA->data_mesgs['record'] as $mesg_key => $mesg) {
|
||||
if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') {
|
||||
echo '<strong><mark><u>';
|
||||
}
|
||||
echo $mesg_key.'<br>';
|
||||
if ($mesg_key == 'speed' || $mesg_key == 'heart_rate') {
|
||||
echo '</strong></mark></u>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><a href="http://www.flotcharts.org/" target="_blank"><i class="fa fa-pie-chart"></i> Flot Charts</a> <small><i class="fa fa-long-arrow-left"></i> click</small></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-md-12">
|
||||
<div id="speed" style="width:100%; height:75px; margin-bottom:8px"></div>
|
||||
<div id="heart_rate" style="width:100%; height:75px; margin-bottom:8px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-map-marker"></i> Google Map</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="gmap" style="padding-bottom:20px; text-align:center;">
|
||||
<strong>Google Geocoding API: </strong><?php echo $location; ?><br>
|
||||
<strong>Google Time Zone API: </strong><?php echo $json_tz->timeZoneName; ?><br><br>
|
||||
<img src="https://maps.googleapis.com/maps/api/staticmap?size=640x480&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE<?php echo $map_string; ?>" alt="Google map" border="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12"><hr></div>
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bug"></i> Debug Information</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<?php $pFFA->showDebugInfo(); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/jquery.flot.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready( function() {
|
||||
var speed_options = {
|
||||
lines: { show: true, fill: true, fillColor: "rgba(11, 98, 164, 0.4)", lineWidth: 1 },
|
||||
points: { show: false },
|
||||
xaxis: {
|
||||
show: false
|
||||
},
|
||||
yaxis: {
|
||||
max: 35,
|
||||
tickFormatter: function(label, series) {
|
||||
return label + ' kmh';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderWidth: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
var speed = {
|
||||
'color': 'rgba(11, 98, 164, 0.8)',
|
||||
'data': [
|
||||
<?php
|
||||
$tmp = [];
|
||||
foreach ($pFFA->data_mesgs['record']['speed'] as $key => $value) {
|
||||
$tmp[] = '['.$key.', '.$value.']';
|
||||
}
|
||||
echo implode(', ', $tmp);
|
||||
?>
|
||||
]
|
||||
};
|
||||
|
||||
var heart_rate_options = {
|
||||
lines: { show: true, fill: true, fillColor: 'rgba(255, 0, 0, .4)', lineWidth: 1 },
|
||||
points: { show: false },
|
||||
xaxis: {
|
||||
show: false
|
||||
},
|
||||
yaxis: {
|
||||
min: 80,
|
||||
tickFormatter: function(label, series) {
|
||||
return label + ' bpm';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderWidth: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
var heart_rate = {
|
||||
'color': 'rgba(255, 0, 0, 0.8)',
|
||||
'data': [
|
||||
<?php
|
||||
unset($tmp);
|
||||
$tmp = [];
|
||||
foreach ($pFFA->data_mesgs['record']['heart_rate'] as $key => $value) {
|
||||
$tmp[] = '['.$key.', '.$value.']';
|
||||
}
|
||||
echo implode(', ', $tmp);
|
||||
?>
|
||||
]
|
||||
};
|
||||
|
||||
$.plot('#speed', [speed], speed_options);
|
||||
$.plot('#heart_rate', [heart_rate], heart_rate_options);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,490 @@
|
||||
<?php
|
||||
/**
|
||||
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
|
||||
* https://github.com/adriangibbons/phpFITFileAnalysis
|
||||
*
|
||||
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
|
||||
*/
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
|
||||
try {
|
||||
$file = '/fit_files/power-analysis.fit';
|
||||
|
||||
$options = [
|
||||
// 'fix_data' => ['all'],
|
||||
// 'units' => ['metric']
|
||||
];
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
|
||||
|
||||
// Google Time Zone API
|
||||
$date = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$date_s = $pFFA->data_mesgs['session']['start_time'];
|
||||
|
||||
$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";
|
||||
|
||||
$result = file_get_contents("$url_tz");
|
||||
$json_tz = json_decode($result);
|
||||
if ($json_tz->status == "OK") {
|
||||
$date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset;
|
||||
}
|
||||
$date->setTimestamp($date_s);
|
||||
|
||||
$ftp = 329;
|
||||
$hr_metrics = $pFFA->hrMetrics(52, 185, 172, 'male');
|
||||
$power_metrics = $pFFA->powerMetrics($ftp);
|
||||
$criticalPower = $pFFA->criticalPower([2,3,5,10,30,60,120,300,600,1200,3600,7200,10800,18000]);
|
||||
$power_histogram = $pFFA->powerHistogram();
|
||||
$power_table = $pFFA->powerPartioned($ftp);
|
||||
$power_pie_chart = $pFFA->partitionData('power', $pFFA->powerZones($ftp), true, false);
|
||||
$quad_plot = $pFFA->quadrantAnalysis(0.175, $ftp);
|
||||
} catch (Exception $e) {
|
||||
echo 'caught exception: '.$e->getMessage();
|
||||
die();
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>phpFITFileAnalysis demo</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
|
||||
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>File: </dt>
|
||||
<dd><?php echo $file; ?></dd>
|
||||
<dt>Device: </dt>
|
||||
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
|
||||
<dt>Sport: </dt>
|
||||
<dd><?php echo $pFFA->sport(); ?></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Recorded: </dt>
|
||||
<dd><?php echo $date->format('D, d-M-y @ g:ia'); ?>
|
||||
</dd>
|
||||
<dt>Duration: </dt>
|
||||
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
|
||||
<dt>Distance: </dt>
|
||||
<dd><?php echo max($pFFA->data_mesgs['record']['distance']); ?> km</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-tachometer"></i> Metrics</h3></a>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-md-5 col-md-offset-1">
|
||||
<h4>Power</h4>
|
||||
<?php
|
||||
foreach ($power_metrics as $key => $value) {
|
||||
echo "$key: $value<br>";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<h4>Heart Rate</h4>
|
||||
<?php
|
||||
foreach ($hr_metrics as $key => $value) {
|
||||
echo "$key: $value<br>";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-line-chart"></i> Critical Power</h3></a>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="criticalPower" style="width:100%; height:300px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Power Distribution (histogram)</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="power_distribution" style="width:100%; height:300px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-pie-chart"></i> Power Zones</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<table id="power_zones_table" class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<th>Zone</th>
|
||||
<th>Zone range</th>
|
||||
<th>% in zone</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$i = 1;
|
||||
foreach ($power_table as $key => $value) {
|
||||
echo '<tr id="'.number_format($value, 1, '-', '').'">';
|
||||
echo '<td>'.$i++.'</td><td>'.$key.' w</td><td>'.$value.' %</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
?>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<div id="power_pie_chart" style="width:100%; height:250px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-line-chart"></i> Quadrant Analysis <small>Circumferential Pedal Velocity (x-axis) vs Average Effective Pedal Force (y-axis)</small></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="quadrant_analysis" style="width:100%; height:600px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/jquery.flot.min.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/jquery.flot.pie.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready( function() {
|
||||
|
||||
var criticalPower_options = {
|
||||
lines: { show: true, fill: true, fillColor: "rgba(11, 98, 164, 0.5)", lineWidth: 1 },
|
||||
points: { show: true },
|
||||
xaxis: {
|
||||
ticks: [2,3,5,10,30,60,120,300,600,1200,3600,7200,10800,18000],
|
||||
transform: function (v) { return Math.log(v); },
|
||||
inverseTransform: function (v) { return Math.exp(v); },
|
||||
tickFormatter: function(label, series) {
|
||||
var hours = parseInt( label / 3600 ) % 24;
|
||||
var minutes = parseInt( label / 60 ) % 60;
|
||||
var seconds = label % 60;
|
||||
var result = (hours > 0 ? hours + "h" : (minutes > 0 ? minutes + "m" : seconds + 's'));
|
||||
return result;
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
tickFormatter: function(label, series) {
|
||||
if(label > 0) return label.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' w';
|
||||
else return '';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
hoverable: true,
|
||||
borderWidth: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var criticalPower = {
|
||||
'color': 'rgba(11, 98, 164, 1)',
|
||||
'data': [
|
||||
<?php
|
||||
foreach ($criticalPower as $key => $value) {
|
||||
echo '['.$key.', '.$value.'], ';
|
||||
}
|
||||
?>
|
||||
]
|
||||
};
|
||||
|
||||
var markings = [{ color: "rgba(203, 75, 75, 1)", lineWidth: 2, xaxis: { from: <?php echo $power_metrics['Normalised Power']; ?>, to: <?php echo $power_metrics['Normalised Power']; ?> } }];
|
||||
|
||||
var power_distribution_options = {
|
||||
points: { show: false },
|
||||
xaxis: {
|
||||
show: true,
|
||||
min: 0,
|
||||
tickSize: 100,
|
||||
tickFormatter: function(label, series) { return label.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' w'; }
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
label: 'time in zone',
|
||||
tickSize: 300,
|
||||
tickFormatter: function(label, series) {
|
||||
if(label == 0) return "";
|
||||
return (label / 60) + ' min';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderWidth: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
},
|
||||
markings: markings
|
||||
},
|
||||
legend: { show: false }
|
||||
};
|
||||
var power_distribution = {
|
||||
'color': 'rgba(77, 167, 77, 0.8)',
|
||||
bars: { show: true, zero: false, barWidth: 25, fillColor: "rgba(77, 167, 77, 0.5)", lineWidth: 1 },
|
||||
'data': [
|
||||
<?php
|
||||
foreach ($power_histogram as $key => $value) {
|
||||
echo '['.$key.', '.$value.'], ';
|
||||
}
|
||||
?>
|
||||
]
|
||||
};
|
||||
|
||||
var power_pie_chart_options = {
|
||||
series: {
|
||||
pie: {
|
||||
radius: 1,
|
||||
show: true,
|
||||
label: {
|
||||
show: true,
|
||||
radius: 3/4,
|
||||
formatter: labelFormatter
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: { hoverable: true },
|
||||
legend: { show: false }
|
||||
};
|
||||
|
||||
function labelFormatter(label, series) {
|
||||
return "<div style='font-size:8pt; text-align:center; padding:2px; color:#333; border-radius: 5px; background-color: #fafafa; border: 1px solid #ddd;'><strong>" + label + "</strong><br/>" + series.data[0][1] + "%</div>";
|
||||
}
|
||||
|
||||
var power_pie_chart = [
|
||||
{
|
||||
label: "Active Recovery",
|
||||
data: <?php echo $power_pie_chart[0]; ?>,
|
||||
"color": "rgba(217, 83, 79, 0.2)"
|
||||
},
|
||||
{
|
||||
label: "Endurance",
|
||||
data: <?php echo $power_pie_chart[1]; ?>,
|
||||
"color": "rgba(217, 83, 79, 0.35)"
|
||||
},
|
||||
{
|
||||
label: "Tempo",
|
||||
data: <?php echo $power_pie_chart[2]; ?>,
|
||||
"color": "rgba(217, 83, 79, 0.5)"
|
||||
},
|
||||
{
|
||||
label: "Threshold",
|
||||
data: <?php echo $power_pie_chart[3]; ?>,
|
||||
"color": "rgba(217, 83, 79, 0.65)"
|
||||
},
|
||||
{
|
||||
label: "VO2max",
|
||||
data: <?php echo $power_pie_chart[4]; ?>,
|
||||
"color": "rgba(217, 83, 79, 0.7)"
|
||||
},
|
||||
{
|
||||
label: "Anaerobic",
|
||||
data: <?php echo $power_pie_chart[5]; ?>,
|
||||
"color": "rgba(217, 83, 79, 0.85)"
|
||||
},
|
||||
{
|
||||
label: "Neuromuscular",
|
||||
data: <?php echo $power_pie_chart[6]; ?>,
|
||||
"color": "rgba(217, 83, 79, 1)"
|
||||
}
|
||||
];
|
||||
|
||||
$("<div id='tooltip_bg'></div>").css({
|
||||
position: "absolute",
|
||||
display: "none",
|
||||
"text-align": "center",
|
||||
"-moz-border-radius": "5px",
|
||||
"-webkit-border-radius": "5px",
|
||||
"border-radius": "5px",
|
||||
"border": "2px solid #fff",
|
||||
padding: "3px 7px",
|
||||
"font-size": "12px",
|
||||
"color": "#fff",
|
||||
"background-color": "#fff"
|
||||
}).appendTo("body");
|
||||
|
||||
$("<div id='tooltip'></div>").css({
|
||||
position: "absolute",
|
||||
display: "none",
|
||||
"text-align": "center",
|
||||
"-moz-border-radius": "5px",
|
||||
"-webkit-border-radius": "5px",
|
||||
"border-radius": "5px",
|
||||
"border": "2px solid",
|
||||
padding: "3px 7px",
|
||||
"font-size": "12px",
|
||||
"color": "#555"
|
||||
}).appendTo("body");
|
||||
|
||||
$("#criticalPower").bind("plothover", function (event, pos, item) {
|
||||
if (item) {
|
||||
var x = item.datapoint[0].toFixed(2),
|
||||
y = item.datapoint[1].toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
|
||||
var currentColor = item.series.color;
|
||||
var lastComma = currentColor.lastIndexOf(',');
|
||||
var newColor = currentColor.slice(0, lastComma + 1) + "0.1)";
|
||||
|
||||
$("#tooltip").html('<strong>' + item.series.xaxis.ticks[item.dataIndex].label + '</strong><br>' + y + ' w')
|
||||
.css({top: item.pageY-45, left: item.pageX+5, "border-color": item.series.color, "background-color": newColor })
|
||||
.fadeIn(200);
|
||||
$("#tooltip_bg").html('<strong>' + item.series.xaxis.ticks[item.dataIndex].label + '</strong><br>' + y + ' w')
|
||||
.css({top: item.pageY-45, left: item.pageX+5 })
|
||||
.fadeIn(200);
|
||||
} else {
|
||||
$("#tooltip").hide();
|
||||
$("#tooltip_bg").hide();
|
||||
}
|
||||
});
|
||||
|
||||
$.plot('#criticalPower', [criticalPower], criticalPower_options);
|
||||
|
||||
var plot_pd = $.plot('#power_distribution', [power_distribution], power_distribution_options);
|
||||
var o = plot_pd.pointOffset({ x: <?php echo $power_metrics['Normalised Power']; ?>, y: plot_pd.height() });
|
||||
$("#power_distribution").append("<span style='background-color: #fafafa; top: 12px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left:" + (o.left + 6) + "px'><strong>normalised power</strong><br><?php echo $power_metrics['Normalised Power']; ?> w</span>");
|
||||
|
||||
$.plot('#power_pie_chart', power_pie_chart, power_pie_chart_options);
|
||||
|
||||
|
||||
$("#power_pie_chart").bind("plothover", function (event, pos, obj) {
|
||||
if (!obj) {
|
||||
$("#power_zones_table tr").removeClass("danger");
|
||||
return;
|
||||
}
|
||||
$("#power_zones_table tr").removeClass("danger");
|
||||
$("#" + obj.series.data[0][1].toFixed(1).toString().replace(/\./g, '-') ).addClass("danger");
|
||||
});
|
||||
|
||||
var quad = [<?php
|
||||
$plottmp = [];
|
||||
foreach ($quad_plot['plot'] as $v) {
|
||||
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
|
||||
}
|
||||
echo implode(', ', $plottmp); ?>];
|
||||
|
||||
var lo = [<?php
|
||||
unset ($plottmp);
|
||||
$plottmp = [];
|
||||
foreach ($quad_plot['ftp-25w'] as $v) {
|
||||
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
|
||||
}
|
||||
echo implode(', ', $plottmp); ?>];
|
||||
|
||||
var at = [<?php
|
||||
unset ($plottmp);
|
||||
$plottmp = [];
|
||||
foreach ($quad_plot['ftp'] as $v) {
|
||||
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
|
||||
}
|
||||
echo implode(', ', $plottmp); ?>];
|
||||
var hi = [<?php
|
||||
unset ($plottmp);
|
||||
$plottmp = [];
|
||||
foreach ($quad_plot['ftp+25w'] as $v) {
|
||||
$plottmp[] = '[' . $v[0] . ', ' . $v[1] . ']';
|
||||
}
|
||||
echo implode(', ', $plottmp); ?>];
|
||||
|
||||
var markings = [
|
||||
{
|
||||
color: "black",
|
||||
lineWidth: 1,
|
||||
xaxis: {
|
||||
from: <?php echo $quad_plot['cpv_threshold']; ?>,
|
||||
to: <?php echo $quad_plot['cpv_threshold']; ?>
|
||||
}
|
||||
},
|
||||
{
|
||||
color: "black",
|
||||
lineWidth: 1,
|
||||
yaxis: {
|
||||
from: <?php echo $quad_plot['aepf_threshold']; ?>,
|
||||
to: <?php echo $quad_plot['aepf_threshold']; ?>
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
var quadrant_analysis_options = {
|
||||
xaxis: {
|
||||
label: 'circumferential pedal velocity',
|
||||
tickFormatter: function(label, series) { return label + ' m/s'; }
|
||||
},
|
||||
yaxis: {
|
||||
max: 400,
|
||||
label: 'average effective pedal force',
|
||||
tickSize: 50,
|
||||
tickFormatter: function(label, series) {
|
||||
if(label == 0) return "";
|
||||
return label + ' N';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderWidth: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
},
|
||||
markings: markings
|
||||
},
|
||||
legend: { show: false }
|
||||
};
|
||||
|
||||
var plot_qa = $.plot($("#quadrant_analysis"), [
|
||||
{
|
||||
data : quad,
|
||||
points : { show: true, radius: 0.25, fill : true, fillColor: "#058DC7" }
|
||||
},
|
||||
{
|
||||
data : at,
|
||||
color: "blue",
|
||||
lines: { show: true, lineWidth: 0.5 }
|
||||
},
|
||||
{
|
||||
data : lo,
|
||||
color: "red",
|
||||
lines: { show: true, lineWidth: 0.5 }
|
||||
},
|
||||
{
|
||||
data : hi,
|
||||
color: "green",
|
||||
lines: { show: true, lineWidth: 0.5 }
|
||||
}
|
||||
], quadrant_analysis_options);
|
||||
|
||||
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 - 40) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: " + (plot_qa.width() - 140) + "px'><strong>High Force / High Velocity</strong><br><?php echo $quad_plot['quad_percent']['hf_hv']; ?> %</span>");
|
||||
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 - 40) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: 50px'><strong>High Force / Low Velocity</strong><br><?php echo $quad_plot['quad_percent']['hf_lv']; ?> %</span>");
|
||||
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 + 15) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: 50px'><strong>Low Force / Low Velocity</strong><br><?php echo $quad_plot['quad_percent']['lf_lv']; ?> %</span>");
|
||||
$("#quadrant_analysis").append("<span style='background-color: #fafafa; top: " + (plot_qa.height() / 2 + 15) + "px; color: #333; text-align: center; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; padding: 3px 7px; position: absolute; left: " + (plot_qa.width() - 140) + "px'><strong>Low Force / High Velocity</strong><br><?php echo $quad_plot['quad_percent']['lf_hv']; ?> %</span>");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,356 @@
|
||||
<?php
|
||||
/**
|
||||
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
|
||||
* https://github.com/adriangibbons/phpFITFileAnalysis
|
||||
*
|
||||
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
|
||||
*/
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
|
||||
try {
|
||||
$file = '/fit_files/power-analysis.fit';
|
||||
|
||||
$options = [
|
||||
// 'fix_data' => ['all'],
|
||||
// 'units' => ['metric']
|
||||
];
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
|
||||
|
||||
// Google Time Zone API
|
||||
$date = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$date_s = $pFFA->data_mesgs['session']['start_time'];
|
||||
|
||||
$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";
|
||||
|
||||
$result = file_get_contents("$url_tz");
|
||||
$json_tz = json_decode($result);
|
||||
if ($json_tz->status == "OK") {
|
||||
$date_s = $date_s + $json_tz->rawOffset + $json_tz->dstOffset;
|
||||
}
|
||||
$date->setTimestamp($date_s);
|
||||
|
||||
$crank_length = 0.175;
|
||||
$ftp = 329;
|
||||
$selected_cadence = 90;
|
||||
|
||||
$json = $pFFA->getJSON($crank_length, $ftp, ['all'], $selected_cadence);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo 'caught exception: '.$e->getMessage();
|
||||
die();
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>phpFITFileAnalysis demo</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/dc.css">
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
|
||||
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>File: </dt>
|
||||
<dd><?php echo $file; ?></dd>
|
||||
<dt>Device: </dt>
|
||||
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
|
||||
<dt>Sport: </dt>
|
||||
<dd><?php echo $pFFA->sport(); ?></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Recorded: </dt>
|
||||
<dd><?php echo $date->format('D, d-M-y @ g:ia'); ?>
|
||||
</dd>
|
||||
<dt>Duration: </dt>
|
||||
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
|
||||
<dt>Distance: </dt>
|
||||
<dd><?php echo max($pFFA->data_mesgs['record']['distance']); ?> km</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<i class="fa fa-bar-chart"></i> Quadrant Analysis
|
||||
<button id="reset-button" type="button" class="btn btn-primary btn-xs pull-right">Reset all filters</button>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="quadrant-analysis-scatter-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Google Map</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="embed-responsive embed-responsive-4by3">
|
||||
<div id="google-map" class="embed-responsive-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Laps</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="lap-row-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Cadence histogram</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="cad-bar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Heart Rate histogram</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="hr-bar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="js/d3.js"></script>
|
||||
<script type="text/javascript" src="js/crossfilter.js"></script>
|
||||
<script type="text/javascript" src="js/dc.js"></script>
|
||||
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=geometry&key=AIzaSyDlPWKTvmHsZ-X6PGsBPAvo0nm1-WdwuYE"></script>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
var ride_data = <?php echo $json; ?>;
|
||||
console.log(ride_data);
|
||||
|
||||
var segmentOverlay;
|
||||
var mapOptions = {
|
||||
zoom: 8
|
||||
};
|
||||
var map = new google.maps.Map(document.getElementById('google-map'), mapOptions);
|
||||
|
||||
var routeCoordinates = [];
|
||||
$.each(ride_data.data, function(k, v) {
|
||||
if(v.position_lat !== null && v.position_long !== null) {
|
||||
routeCoordinates.push(new google.maps.LatLng(v.position_lat, v.position_long));
|
||||
}
|
||||
});
|
||||
|
||||
var routePath = new google.maps.Polyline({
|
||||
path: routeCoordinates,
|
||||
geodesic: true,
|
||||
strokeColor: '#FF0000',
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 3
|
||||
});
|
||||
routePath.setMap(map);
|
||||
|
||||
var bounds = new google.maps.LatLngBounds();
|
||||
for (var i = 0; i < routeCoordinates.length; i++) {
|
||||
bounds.extend(routeCoordinates[i]);
|
||||
}
|
||||
map.fitBounds(bounds);
|
||||
|
||||
google.maps.event.addDomListener(window, "resize", function() {
|
||||
var center = map.getCenter();
|
||||
google.maps.event.trigger(map, "resize");
|
||||
map.setCenter(center);
|
||||
});
|
||||
|
||||
function updateOverlay(data) {
|
||||
if(typeof segmentOverlay !== 'undefined') {
|
||||
segmentOverlay.setMap(null);
|
||||
}
|
||||
var segmentCoords = [];
|
||||
$.each(data, function(k, v) {
|
||||
if(v.position_lat !== null && v.position_long !== null) {
|
||||
segmentCoords.push(new google.maps.LatLng(v.position_lat, v.position_long));
|
||||
}
|
||||
});
|
||||
|
||||
segmentOverlay = new google.maps.Polyline({
|
||||
path: segmentCoords,
|
||||
geodesic: true,
|
||||
strokeColor: '#0000FF',
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 3
|
||||
});
|
||||
segmentOverlay.setMap(map);
|
||||
|
||||
var bounds = new google.maps.LatLngBounds();
|
||||
for (var i = 0; i < segmentCoords.length; i++) {
|
||||
bounds.extend(segmentCoords[i]);
|
||||
}
|
||||
|
||||
map.fitBounds(bounds);
|
||||
}
|
||||
|
||||
var qaScatterChart = dc.seriesChart("#quadrant-analysis-scatter-chart"),
|
||||
lapRowChart = dc.rowChart("#lap-row-chart"),
|
||||
cadBarChart = dc.barChart("#cad-bar-chart"),
|
||||
hrBarChart = dc.barChart("#hr-bar-chart");
|
||||
|
||||
var ndx,
|
||||
tsDim, latlngDim, qaDim, lapDim, cadDim, hrDim,
|
||||
qaGrp, lapGrp, cadGrp, hrGrp;
|
||||
|
||||
ndx = crossfilter(ride_data.data);
|
||||
|
||||
tsDim = ndx.dimension(function(d) {return d.timestamp;});
|
||||
|
||||
latlngDim = ndx.dimension(function(d) {return [d.position_lat, d.position_long];});
|
||||
|
||||
qaDim = ndx.dimension(function(d) {return [d.cpv, d.aepf, d.lap];});
|
||||
qaGrp = qaDim.group().reduceSum(function(d) { return d.lap; });
|
||||
|
||||
lapDim = ndx.dimension(function(d) {return d.lap;});
|
||||
lapGrp = lapDim.group();
|
||||
|
||||
cadDim = ndx.dimension(function(d) {return d.cadence;});
|
||||
cadGrp = cadDim.group();
|
||||
|
||||
hrDim = ndx.dimension(function(d) {return d.heart_rate;});
|
||||
hrGrp = hrDim.group();
|
||||
|
||||
// Quadrant Analysis chart
|
||||
var subChart = function(c) {
|
||||
return dc.scatterPlot(c)
|
||||
.symbolSize(5)
|
||||
.highlightedSize(8)
|
||||
};
|
||||
|
||||
qaScatterChart
|
||||
.width(550)
|
||||
.height(388)
|
||||
.chart(subChart)
|
||||
.x(d3.scale.linear().domain([0,2.5]))
|
||||
.brushOn(false)
|
||||
.yAxisLabel("Average Effective Pedal Force (N)")
|
||||
.xAxisLabel("Circumferential Pedal Velocity (m/s)")
|
||||
.elasticY(true)
|
||||
.dimension(qaDim)
|
||||
.group(qaGrp)
|
||||
.seriesAccessor(function(d) {return "Lap: " + d.key[2];})
|
||||
.keyAccessor(function(d) {return d.key[0];})
|
||||
.valueAccessor(function(d) {return d.key[1];})
|
||||
.legend(dc.legend().x(450).y(50).itemHeight(13).gap(5).horizontal(1).legendWidth(70).itemWidth(70));
|
||||
qaScatterChart.margins().left += 20;
|
||||
qaScatterChart.margins().bottom += 10;
|
||||
|
||||
var hght = (lapGrp.size() * 40) > 76 ? lapGrp.size() * 40 : 76;
|
||||
// Lap chart
|
||||
lapRowChart
|
||||
.width(375).height(hght)
|
||||
.dimension(lapDim)
|
||||
.group(lapGrp)
|
||||
.elasticX(true)
|
||||
.gap(2)
|
||||
.label(function(d) {
|
||||
var hours = parseInt(d.value / 3600) % 24;
|
||||
var minutes = parseInt(d.value / 60 ) % 60;
|
||||
var seconds = d.value % 60;
|
||||
return 'Lap ' + d.key + ' (' + ((hours > 0 ? hours + 'h ' : '') + (minutes < 10 ? "0" + minutes : minutes) + "m " + (seconds < 10 ? "0" + seconds : seconds) + 's)');
|
||||
});
|
||||
|
||||
// Cadence chart
|
||||
cadBarChart
|
||||
.width(375).height(150)
|
||||
.dimension(cadDim)
|
||||
.group(cadGrp)
|
||||
.x(d3.scale.linear().domain([40,cadDim.top(1)[0].cadence]))
|
||||
.elasticY(true);
|
||||
cadBarChart.margins().left = 45;
|
||||
|
||||
cadBarChart.yAxis()
|
||||
.tickFormat(function(d) {
|
||||
var hours = parseInt(d / 3600) % 24;
|
||||
var minutes = parseInt(d / 60 ) % 60;
|
||||
var seconds = d % 60;
|
||||
return (hours > 0 ? hours + ':' : '') + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
|
||||
})
|
||||
.ticks(4);
|
||||
|
||||
// HR chart
|
||||
hrBarChart
|
||||
.width(375).height(150)
|
||||
.dimension(hrDim)
|
||||
.group(hrGrp)
|
||||
.x(d3.scale.linear().domain([hrDim.bottom(1)[0].heart_rate,hrDim.top(1)[0].heart_rate]))
|
||||
.elasticY(true);
|
||||
hrBarChart.margins().left = 45;
|
||||
|
||||
hrBarChart.yAxis()
|
||||
.tickFormat(function(d) {
|
||||
var hours = parseInt(d / 3600) % 24;
|
||||
var minutes = parseInt(d / 60 ) % 60;
|
||||
var seconds = d % 60;
|
||||
return (hours > 0 ? hours + ':' : '') + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
|
||||
})
|
||||
.ticks(4);
|
||||
|
||||
dc.renderAll();
|
||||
|
||||
lapRowChart.on('filtered', function(chart, filter){
|
||||
updateOverlay(tsDim.bottom(Infinity));
|
||||
});
|
||||
|
||||
qaScatterChart.on('renderlet', function(chart) {
|
||||
var horizontal_line = [{x: chart.x().range()[0], y: chart.y()(ride_data.aepf_threshold)},
|
||||
{x: chart.x().range()[1], y: chart.y()(ride_data.aepf_threshold)}];
|
||||
var vertical_line = [{x: chart.x()(ride_data.cpv_threshold), y: chart.y().range()[0]},
|
||||
{x: chart.x()(ride_data.cpv_threshold), y: chart.y().range()[1]}];
|
||||
var line = d3.svg.line()
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
// .interpolate('linear');
|
||||
|
||||
var path = chart.select('g.chart-body').selectAll('path.aepf_threshold').data([horizontal_line]);
|
||||
path.enter().append('path').attr('class', 'aepf_threshold').attr('stroke', 'black');
|
||||
path.attr('d', line);
|
||||
|
||||
var path2 = chart.select('g.chart-body').selectAll('path.cpv_threshold').data([vertical_line]);
|
||||
path2.enter().append('path').attr('class', 'cpv_threshold').attr('stroke', 'black');
|
||||
path2.attr('d', line);
|
||||
});
|
||||
|
||||
$('#reset-button').on('click', function(e) {
|
||||
e.preventDefault(); // preventing default click action
|
||||
dc.filterAll();
|
||||
dc.redrawAll();
|
||||
if(typeof segmentOverlay !== 'undefined') {
|
||||
segmentOverlay.setMap(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* Demonstration of the phpFITFileAnalysis class using Twitter Bootstrap framework
|
||||
* https://github.com/adriangibbons/phpFITFileAnalysis
|
||||
*
|
||||
* If you find this useful, feel free to drop me a line at Adrian.GitHub@gmail.com
|
||||
*/
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
try {
|
||||
$file = '/fit_files/swim.fit';
|
||||
|
||||
$options = [
|
||||
// 'fix_data' => [],
|
||||
'units' => 'raw',
|
||||
// 'pace' => false
|
||||
];
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis(__DIR__ . $file, $options);
|
||||
} catch (Exception $e) {
|
||||
echo 'caught exception: '.$e->getMessage();
|
||||
die();
|
||||
}
|
||||
$units = 'm';
|
||||
$pool_length = $pFFA->data_mesgs['session']['pool_length'];
|
||||
$total_distance = number_format($pFFA->data_mesgs['record']['distance']);
|
||||
if ($pFFA->enumData('display_measure', $pFFA->data_mesgs['session']['pool_length_unit']) == 'statute') {
|
||||
$pool_length = round($pFFA->data_mesgs['session']['pool_length'] * 1.0936133);
|
||||
$total_distance = number_format($pFFA->data_mesgs['record']['distance'] * 1.0936133);
|
||||
$units = 'yd';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>phpFITFileAnalysis demo</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h2><strong>phpFITFileAnalysis </strong><small>A PHP class for analysing FIT files created by Garmin GPS devices.</small></h2>
|
||||
<p>This is a demonstration of the phpFITFileAnalysis class available on <a class="btn btn-default btn-lg" href="https://github.com/adriangibbons/phpFITFileAnalysis" target="_blank" role="button"><i class="fa fa-github"></i> GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-file-code-o"></i> FIT File info</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>File: </dt>
|
||||
<dd><?php echo $file; ?></dd>
|
||||
<dt>Device: </dt>
|
||||
<dd><?php echo $pFFA->manufacturer() . ' ' . $pFFA->product(); ?></dd>
|
||||
<dt>Sport: </dt>
|
||||
<dd><?php echo $pFFA->sport(); ?></dd>
|
||||
<dt>Pool length: </dt>
|
||||
<dd><?php echo $pool_length.' '.$units; ?></dd>
|
||||
<dt>Duration: </dt>
|
||||
<dd><?php echo gmdate('H:i:s', $pFFA->data_mesgs['session']['total_elapsed_time']); ?></dd>
|
||||
<dt>Total distance: </dt>
|
||||
<dd><?php echo $total_distance.' '.$units; ?></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-bar-chart"></i> Lap Time vs. Number of Strokes</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="lap_times" style="width:100%; height:200px; margin-bottom:8px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-tags"></i> Length Message fields</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<th>Length</th>
|
||||
<th>Time (min:sec)</th>
|
||||
<th># Strokes</th>
|
||||
<th>Stroke</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$lengths = count($pFFA->data_mesgs['length']['total_timer_time']);
|
||||
$active_length = 0;
|
||||
for ($i=0; $i<$lengths; $i++) {
|
||||
$min = floor($pFFA->data_mesgs['length']['total_timer_time'][$i] / 60);
|
||||
$sec = number_format($pFFA->data_mesgs['length']['total_timer_time'][$i] - ($min*60), 1);
|
||||
$dur = $min.':'.$sec;
|
||||
if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') {
|
||||
echo '<tr>';
|
||||
echo '<td>'.($i+1).'</td>';
|
||||
echo '<td>'.$dur.'</td>';
|
||||
echo '<td>'.$pFFA->data_mesgs['length']['total_strokes'][$i].'</td>';
|
||||
echo '<td>'.$pFFA->enumData('swim_stroke', $pFFA->data_mesgs['length']['swim_stroke'][$active_length]).'</td>';
|
||||
echo '<td></td>';
|
||||
echo '</tr>';
|
||||
$active_length++;
|
||||
} else {
|
||||
echo '<tr class="danger">';
|
||||
echo '<td>'.($i+1).'</td>';
|
||||
echo '<td>'.$dur.'</td>';
|
||||
echo '<td>-</td>';
|
||||
echo '<td>Rest</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="js/jquery.flot.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready( function() {
|
||||
var chart_options = {
|
||||
xaxis: {
|
||||
show: false
|
||||
},
|
||||
yaxes: [ { transform: function (v) { return -v; }, inverseTransform: function (v) { return -v; }, tickFormatter: function(label, series) { return label + ' s'; } },
|
||||
{ alignTicksWithAxis: 1, position: "right", } ],
|
||||
grid: {
|
||||
borderWidth: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
var lap_times = {
|
||||
'color': 'rgba(255, 0, 0, 1)',
|
||||
'label': 'Lap Time',
|
||||
'data': [
|
||||
<?php
|
||||
$tmp = [];
|
||||
for ($i=0; $i<$lengths; $i++) {
|
||||
if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') {
|
||||
$tmp[] = '['.$i.', '.$pFFA->data_mesgs['length']['total_timer_time'][$i].']';
|
||||
}
|
||||
}
|
||||
echo implode(', ', $tmp);
|
||||
?>
|
||||
],
|
||||
lines: { show: true, fill: false, lineWidth: 2 },
|
||||
points: { show: false }
|
||||
};
|
||||
|
||||
var num_strokes = {
|
||||
'color': 'rgba(11, 98, 164, 0.5)',
|
||||
'label': 'Number of Strokes',
|
||||
'data': [
|
||||
<?php
|
||||
$tmp = [];
|
||||
for ($i=0; $i<$lengths; $i++) {
|
||||
if ($pFFA->enumData('length_type', $pFFA->data_mesgs['length']['length_type'][$i]) == 'active') {
|
||||
$tmp[] = '['.$i.', '.$pFFA->data_mesgs['length']['total_strokes'][$i].']';
|
||||
}
|
||||
}
|
||||
echo implode(', ', $tmp);
|
||||
?>
|
||||
],
|
||||
bars: { show: true, fill: true, fillColor: "rgba(11, 98, 164, 0.3)", lineWidth: 1 },
|
||||
points: { show: false },
|
||||
yaxis: 2
|
||||
};
|
||||
|
||||
$.plot('#lap_times', [lap_times, num_strokes], chart_options);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class BasicTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $demo_files = [];
|
||||
private $valid_files = ['mountain-biking.fit', 'power-analysis.fit', 'road-cycling.fit', 'swim.fit'];
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
}
|
||||
|
||||
public function testDemoFilesExist()
|
||||
{
|
||||
$this->demo_files = array_values(array_diff(scandir($this->base_dir), array('..', '.')));
|
||||
sort($this->demo_files);
|
||||
sort($this->valid_files);
|
||||
$this->assertEquals($this->valid_files, $this->demo_files);
|
||||
var_dump($this->demo_files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testEmptyFilepath()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testFileDoesntExist()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis('file_doesnt_exist.fit');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testInvalidFitFile()
|
||||
{
|
||||
$file_path = $this->base_dir . '../composer.json';
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($file_path);
|
||||
}
|
||||
|
||||
|
||||
public function testDemoFileBasics()
|
||||
{
|
||||
foreach($this->demo_files as $filename) {
|
||||
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $filename);
|
||||
|
||||
$this->assertGreaterThan(0, $pFFA->data_mesgs['activity']['timestamp'], 'No Activity timestamp!');
|
||||
|
||||
if (isset($pFFA->data_mesgs['record'])) {
|
||||
$this->assertGreaterThan(0, count($pFFA->data_mesgs['record']['timestamp']), 'No Record timestamps!');
|
||||
|
||||
// Check if distance from record messages is +/- 2% of distance from session message
|
||||
if (is_array($pFFA->data_mesgs['record']['distance'])) {
|
||||
$distance_difference = abs(end($pFFA->data_mesgs['record']['distance']) - $pFFA->data_mesgs['session']['total_distance'] / 1000);
|
||||
$this->assertLessThan(0.02 * end($pFFA->data_mesgs['record']['distance']), $distance_difference, 'Session distance should be similar to last Record distance');
|
||||
}
|
||||
|
||||
// Look for big jumps in latitude and longitude
|
||||
if (isset($pFFA->data_mesgs['record']['position_lat']) && is_array($pFFA->data_mesgs['record']['position_lat'])) {
|
||||
foreach ($pFFA->data_mesgs['record']['position_lat'] as $key => $value) {
|
||||
if (isset($pFFA->data_mesgs['record']['position_lat'][$key - 1])) {
|
||||
if (abs($pFFA->data_mesgs['record']['position_lat'][$key - 1] - $pFFA->data_mesgs['record']['position_lat'][$key]) > 1) {
|
||||
$this->assertTrue(false, 'Too big a jump in latitude');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($pFFA->data_mesgs['record']['position_long']) && is_array($pFFA->data_mesgs['record']['position_long'])) {
|
||||
foreach ($pFFA->data_mesgs['record']['position_long'] as $key => $value) {
|
||||
if (isset($pFFA->data_mesgs['record']['position_long'][$key - 1])) {
|
||||
if (abs($pFFA->data_mesgs['record']['position_long'][$key - 1] - $pFFA->data_mesgs['record']['position_long'][$key]) > 1) {
|
||||
$this->assertTrue(false, 'Too big a jump in longitude');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class EnumDataTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'swim.fit';
|
||||
private $pFFA;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
|
||||
}
|
||||
|
||||
public function testEnumData_manufacturer()
|
||||
{
|
||||
$this->assertEquals('Garmin', $this->pFFA->manufacturer());
|
||||
}
|
||||
|
||||
public function testEnumData_product()
|
||||
{
|
||||
$this->assertEquals('Forerunner 910XT', $this->pFFA->product());
|
||||
}
|
||||
|
||||
public function testEnumData_sport()
|
||||
{
|
||||
$this->assertEquals('Swimming', $this->pFFA->sport());
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class FixDataTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'road-cycling.fit';
|
||||
private $filename2 = 'power-analysis.fit';
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Original road-cycling.fit before fixData() contains:
|
||||
*
|
||||
* record message | count()
|
||||
* -----------------+--------
|
||||
* timestamp | 4317
|
||||
* position_lat | 4309 <- test
|
||||
* position_long | 4309 <- test
|
||||
* distance | 4309 <- test
|
||||
* altitude | 4317
|
||||
* speed | 4309 <- test
|
||||
* heart_rate | 4316 <- test
|
||||
* temperature | 4317
|
||||
*/
|
||||
public function testFixData_before()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename);
|
||||
|
||||
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['position_lat']));
|
||||
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['position_long']));
|
||||
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['distance']));
|
||||
$this->assertEquals(4309, count($pFFA->data_mesgs['record']['speed']));
|
||||
$this->assertEquals(4316, count($pFFA->data_mesgs['record']['heart_rate']));
|
||||
|
||||
$pFFA2 = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename2);
|
||||
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['cadence']));
|
||||
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['power']));
|
||||
}
|
||||
|
||||
/**
|
||||
* $pFFA->data_mesgs['record']['heart_rate']
|
||||
* [805987191 => 118],
|
||||
* [805987192 => missing],
|
||||
* [805987193 => 117]
|
||||
*/
|
||||
public function testFixData_hr_missing_key()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename);
|
||||
|
||||
$hr_missing_key = array_diff($pFFA->data_mesgs['record']['timestamp'], array_keys($pFFA->data_mesgs['record']['heart_rate']));
|
||||
$this->assertEquals([3036 => 1437052792], $hr_missing_key);
|
||||
}
|
||||
|
||||
public function testFixData_after()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['all']]);
|
||||
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['position_lat']));
|
||||
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['position_long']));
|
||||
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['distance']));
|
||||
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['speed']));
|
||||
$this->assertEquals(4317, count($pFFA->data_mesgs['record']['heart_rate']));
|
||||
|
||||
$pFFA2 = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename2, ['fix_data' => ['cadence', 'power']]);
|
||||
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['cadence']));
|
||||
$this->assertEquals(3043, count($pFFA2->data_mesgs['record']['power']));
|
||||
}
|
||||
|
||||
/**
|
||||
* $pFFA->data_mesgs['record']['heart_rate']
|
||||
* [805987191 => 118],
|
||||
* [805987192 => 117.5],
|
||||
* [805987193 => 117]
|
||||
*/
|
||||
public function testFixData_hr_missing_key_fixed()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['heart_rate']]);
|
||||
|
||||
$this->assertEquals(117.5, $pFFA->data_mesgs['record']['heart_rate'][1437052792]);
|
||||
}
|
||||
|
||||
public function testFixData_validate_options_pass()
|
||||
{
|
||||
// Positive testing
|
||||
$valid_options = ['all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'speed', 'power'];
|
||||
foreach($valid_options as $valid_option) {
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => [$valid_option]]);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFixData_data_every_second()
|
||||
{
|
||||
$options = [
|
||||
'fix_data' => ['speed'],
|
||||
'data_every_second' => true,
|
||||
'units' => 'raw',
|
||||
];
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, $options);
|
||||
|
||||
$this->assertEquals(6847, count($pFFA->data_mesgs['record']['speed']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testFixData_validate_options_fail()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['fix_data' => ['INVALID']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testFixData_invalid_pace_option()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 'INVALID']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testFixData_invalid_pace_option2()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 123456]);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class GetJSONTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'power-analysis.fit';
|
||||
private $pFFA;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
|
||||
}
|
||||
|
||||
public function testGetJSON()
|
||||
{
|
||||
// getJSON() create a JSON object that contains available record message information.
|
||||
$crank_length = null;
|
||||
$ftp = null;
|
||||
$data_required = ['timestamp', 'speed'];
|
||||
$selected_cadence = 90;
|
||||
$php_object = json_decode($this->pFFA->getJSON($crank_length, $ftp, $data_required, $selected_cadence));
|
||||
|
||||
// Assert data
|
||||
$this->assertEquals('raw', $php_object->units);
|
||||
$this->assertEquals(3043, count($php_object->data));
|
||||
$this->assertEquals(1437474517, $php_object->data[0]->timestamp);
|
||||
$this->assertEquals(1.378, $php_object->data[0]->speed);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class HRTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'power-analysis.fit';
|
||||
private $pFFA;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
|
||||
}
|
||||
|
||||
public function testHR_hrMetrics()
|
||||
{
|
||||
$hr_metrics = $this->pFFA->hrMetrics(50, 190, 170, 'male');
|
||||
|
||||
$this->assertEquals(74, $hr_metrics['TRIMPexp']);
|
||||
$this->assertEquals(0.8, $hr_metrics['hrIF']);
|
||||
}
|
||||
|
||||
public function testHR_hrPartionedHRmaximum()
|
||||
{
|
||||
// Calls phpFITFileAnalysis::hrZonesMax()
|
||||
$hr_partioned_HRmaximum = $this->pFFA->hrPartionedHRmaximum(190);
|
||||
|
||||
$this->assertEquals(19.4, $hr_partioned_HRmaximum['0-113']);
|
||||
$this->assertEquals(33.1, $hr_partioned_HRmaximum['114-142']);
|
||||
$this->assertEquals(31.4, $hr_partioned_HRmaximum['143-161']);
|
||||
$this->assertEquals(16.1, $hr_partioned_HRmaximum['162-180']);
|
||||
$this->assertEquals(0, $hr_partioned_HRmaximum['181+']);
|
||||
}
|
||||
|
||||
public function testHR_hrPartionedHRreserve()
|
||||
{
|
||||
// Calls phpFITFileAnalysis::hrZonesReserve()
|
||||
$hr_partioned_HRreserve = $this->pFFA->hrPartionedHRreserve(50, 190);
|
||||
|
||||
$this->assertEquals(45.1, $hr_partioned_HRreserve['0-133']);
|
||||
$this->assertEquals(5.8, $hr_partioned_HRreserve['134-140']);
|
||||
$this->assertEquals(20.1, $hr_partioned_HRreserve['141-154']);
|
||||
$this->assertEquals(15.9, $hr_partioned_HRreserve['155-164']);
|
||||
$this->assertEquals(12.5, $hr_partioned_HRreserve['165-174']);
|
||||
$this->assertEquals(0.6, $hr_partioned_HRreserve['175-181']);
|
||||
$this->assertEquals(0, $hr_partioned_HRreserve['182+']);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class IsPausedTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'power-analysis.fit';
|
||||
private $pFFA;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
|
||||
}
|
||||
|
||||
public function testIsPaused()
|
||||
{
|
||||
// isPaused() returns array of booleans using timestamp as key.
|
||||
$is_paused = $this->pFFA->isPaused();
|
||||
|
||||
// Assert number of timestamps
|
||||
$this->assertEquals(3190, count($is_paused));
|
||||
|
||||
// Assert an arbitrary element/timestamps is true
|
||||
$this->assertEquals(true, $is_paused[1437477706]);
|
||||
|
||||
// Assert an arbitrary element/timestamps is false
|
||||
$this->assertEquals(false, $is_paused[1437474517]);
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class PowerTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'power-analysis.fit';
|
||||
private $pFFA;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
|
||||
}
|
||||
|
||||
public function testPower_criticalPower_values()
|
||||
{
|
||||
$time_periods = [2,5,10,60,300,600,1200,1800,3600];
|
||||
$cps = $this->pFFA->criticalPower($time_periods);
|
||||
|
||||
array_walk($cps, function(&$v) { $v = round($v, 2); });
|
||||
|
||||
$this->assertEquals(551.50, $cps[2]);
|
||||
$this->assertEquals(542.20, $cps[5]);
|
||||
$this->assertEquals(527.70, $cps[10]);
|
||||
$this->assertEquals(452.87, $cps[60]);
|
||||
$this->assertEquals(361.99, $cps[300]);
|
||||
$this->assertEquals(328.86, $cps[600]);
|
||||
$this->assertEquals(260.52, $cps[1200]);
|
||||
$this->assertEquals(221.81, $cps[1800]);
|
||||
}
|
||||
|
||||
public function testPower_criticalPower_time_period_max()
|
||||
{
|
||||
// 14400 seconds is 4 hours and longer than file duration so should only get one result back (for 2 seconds)
|
||||
$time_periods = [2,14400];
|
||||
$cps = $this->pFFA->criticalPower($time_periods);
|
||||
|
||||
$this->assertEquals(1, count($cps));
|
||||
}
|
||||
|
||||
public function testPower_powerMetrics()
|
||||
{
|
||||
$power_metrics = $this->pFFA->powerMetrics(350);
|
||||
|
||||
$this->assertEquals(221, $power_metrics['Average Power']);
|
||||
$this->assertEquals(671, $power_metrics['Kilojoules']);
|
||||
$this->assertEquals(285, $power_metrics['Normalised Power']);
|
||||
$this->assertEquals(1.29, $power_metrics['Variability Index']);
|
||||
$this->assertEquals(0.81, $power_metrics['Intensity Factor']);
|
||||
$this->assertEquals(56, $power_metrics['Training Stress Score']);
|
||||
}
|
||||
|
||||
public function testPower_power_partitioned()
|
||||
{
|
||||
// Calls phpFITFileAnalysis::powerZones();
|
||||
$power_partioned = $this->pFFA->powerPartioned(350);
|
||||
|
||||
$this->assertEquals(45.2, $power_partioned['0-193']);
|
||||
$this->assertEquals(10.8, $power_partioned['194-263']);
|
||||
$this->assertEquals(18.1, $power_partioned['264-315']);
|
||||
$this->assertEquals(17.9, $power_partioned['316-368']);
|
||||
$this->assertEquals(4.2, $power_partioned['369-420']);
|
||||
$this->assertEquals(3.3, $power_partioned['421-525']);
|
||||
$this->assertEquals(0.4, $power_partioned['526+']);
|
||||
}
|
||||
|
||||
public function testPower_powerHistogram()
|
||||
{
|
||||
// Calls phpFITFileAnalysis::histogram();
|
||||
$power_histogram = $this->pFFA->powerHistogram(100);
|
||||
|
||||
$this->assertEquals(374, $power_histogram[0]);
|
||||
$this->assertEquals(634, $power_histogram[100]);
|
||||
$this->assertEquals(561, $power_histogram[200]);
|
||||
$this->assertEquals(1103, $power_histogram[300]);
|
||||
$this->assertEquals(301, $power_histogram[400]);
|
||||
$this->assertEquals(66, $power_histogram[500]);
|
||||
$this->assertEquals(4, $power_histogram[600]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_criticalPower_no_power()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
|
||||
|
||||
$time_periods = [2,14400];
|
||||
$cps = $pFFA->criticalPower($time_periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_powerMetrics_no_power()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
|
||||
|
||||
$power_metrics = $pFFA->powerMetrics(350);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_powerHistogram_no_power()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
|
||||
|
||||
$power_metrics = $pFFA->powerHistogram(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_powerHistogram_invalid_bucket_width()
|
||||
{
|
||||
$power_histogram = $this->pFFA->powerHistogram('INVALID');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_power_partitioned_no_power()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . 'road-cycling.fit');
|
||||
|
||||
$power_partioned = $pFFA->powerPartioned(350);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_power_partitioned_not_array()
|
||||
{
|
||||
$power_histogram = $this->pFFA->partitionData('power', 123456);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_power_partitioned_not_numeric()
|
||||
{
|
||||
$power_histogram = $this->pFFA->partitionData('power', [200, 400, 'INVALID']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testPower_power_partitioned_not_ascending()
|
||||
{
|
||||
$power_histogram = $this->pFFA->partitionData('power', [400, 200]);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class QuadrantAnalysisTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'power-analysis.fit';
|
||||
private $pFFA;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
$this->pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw']);
|
||||
}
|
||||
|
||||
public function testQuadrantAnalysis()
|
||||
{
|
||||
$crank_length = 0.175;
|
||||
$ftp = 329;
|
||||
$selected_cadence = 90;
|
||||
$use_timestamps = false;
|
||||
|
||||
// quadrantAnalysis() returns an array that can be used to plot CPV vs AEPF.
|
||||
$quadrant_plot = $this->pFFA->quadrantAnalysis($crank_length, $ftp, $selected_cadence, $use_timestamps);
|
||||
|
||||
$this->assertEquals(90, $quadrant_plot['selected_cadence']);
|
||||
|
||||
$this->assertEquals(199.474, $quadrant_plot['aepf_threshold']);
|
||||
$this->assertEquals(1.649, $quadrant_plot['cpv_threshold']);
|
||||
|
||||
$this->assertEquals(10.48, $quadrant_plot['quad_percent']['hf_hv']);
|
||||
$this->assertEquals(10.61, $quadrant_plot['quad_percent']['hf_lv']);
|
||||
$this->assertEquals(14.00, $quadrant_plot['quad_percent']['lf_hv']);
|
||||
$this->assertEquals(64.91, $quadrant_plot['quad_percent']['lf_lv']);
|
||||
|
||||
$this->assertEquals(1.118, $quadrant_plot['plot'][0][0]);
|
||||
$this->assertEquals(47.411, $quadrant_plot['plot'][0][1]);
|
||||
|
||||
$this->assertEquals(0.367, $quadrant_plot['ftp-25w'][0][0]);
|
||||
$this->assertEquals(829.425, $quadrant_plot['ftp-25w'][0][1]);
|
||||
|
||||
$this->assertEquals(0.367, $quadrant_plot['ftp'][0][0]);
|
||||
$this->assertEquals(897.634, $quadrant_plot['ftp'][0][1]);
|
||||
|
||||
$this->assertEquals(0.367, $quadrant_plot['ftp+25w'][0][0]);
|
||||
$this->assertEquals(965.843, $quadrant_plot['ftp+25w'][0][1]);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
if(!class_exists('adriangibbons\phpFITFileAnalysis')) {
|
||||
require __DIR__ . '/../src/phpFITFileAnalysis.php';
|
||||
}
|
||||
|
||||
class SetUnitsTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $base_dir;
|
||||
private $filename = 'road-cycling.fit';
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->base_dir = __DIR__ . '/../demo/fit_files/';
|
||||
}
|
||||
|
||||
public function testSetUnits_validate_options_pass()
|
||||
{
|
||||
$valid_options = ['raw', 'statute', 'metric'];
|
||||
foreach($valid_options as $valid_option) {
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => $valid_option]);
|
||||
|
||||
if($valid_option === 'raw') {
|
||||
$this->assertEquals(1.286, reset($pFFA->data_mesgs['record']['speed']));
|
||||
}
|
||||
if($valid_option === 'statute') {
|
||||
$this->assertEquals(2.877, reset($pFFA->data_mesgs['record']['speed']));
|
||||
}
|
||||
if($valid_option === 'metric') {
|
||||
$this->assertEquals(4.63, reset($pFFA->data_mesgs['record']['speed']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testSetUnits_validate_options_fail()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'INVALID']);
|
||||
}
|
||||
|
||||
public function testSetUnits_validate_pace_option_pass()
|
||||
{
|
||||
$valid_options = [true, false];
|
||||
foreach($valid_options as $valid_option) {
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['units' => 'raw', 'pace' => $valid_option]);
|
||||
|
||||
$this->assertEquals(1.286, reset($pFFA->data_mesgs['record']['speed']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
*/
|
||||
public function testSetUnits_validate_pace_option_fail()
|
||||
{
|
||||
$pFFA = new adriangibbons\phpFITFileAnalysis($this->base_dir . $this->filename, ['pace' => 'INVALID']);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 70205)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
{
|
||||
"active": true,
|
||||
"name": "Instantiator",
|
||||
"slug": "instantiator",
|
||||
"docsSlug": "doctrine-instantiator",
|
||||
"codePath": "/src",
|
||||
"versions": [
|
||||
{
|
||||
"name": "1.5",
|
||||
"branchName": "1.5.x",
|
||||
"slug": "latest",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "1.4",
|
||||
"branchName": "1.4.x",
|
||||
"slug": "1.4",
|
||||
"aliases": [
|
||||
"current",
|
||||
"stable"
|
||||
],
|
||||
"maintained": true,
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "1.3",
|
||||
"branchName": "1.3.x",
|
||||
"slug": "1.3",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "1.2",
|
||||
"branchName": "1.2.x",
|
||||
"slug": "1.2"
|
||||
},
|
||||
{
|
||||
"name": "1.1",
|
||||
"branchName": "1.1.x",
|
||||
"slug": "1.1"
|
||||
},
|
||||
{
|
||||
"name": "1.0",
|
||||
"branchName": "1.0.x",
|
||||
"slug": "1.0"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
* Follow the [Doctrine Coding Standard](https://github.com/doctrine/coding-standard)
|
||||
* The project will follow strict [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php)
|
||||
* Any contribution must provide tests for additional introduced conditions
|
||||
* Any un-confirmed issue needs a failing test case before being accepted
|
||||
* Pull requests must be sent from a new hotfix/feature branch, not from `master`.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the project and run the tests, you need to clone it first:
|
||||
|
||||
```sh
|
||||
$ git clone git://github.com/doctrine/instantiator.git
|
||||
```
|
||||
|
||||
You will then need to run a composer installation:
|
||||
|
||||
```sh
|
||||
$ cd Instantiator
|
||||
$ curl -s https://getcomposer.org/installer | php
|
||||
$ php composer.phar update
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The PHPUnit version to be used is the one installed as a dev- dependency via composer:
|
||||
|
||||
```sh
|
||||
$ ./vendor/bin/phpunit
|
||||
```
|
||||
|
||||
Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement
|
||||
won't be merged.
|
||||
|
@ -1,19 +0,0 @@
|
||||
Copyright (c) 2014 Doctrine Project
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,38 +0,0 @@
|
||||
# Instantiator
|
||||
|
||||
This library provides a way of avoiding usage of constructors when instantiating PHP classes.
|
||||
|
||||
[](https://travis-ci.org/doctrine/instantiator)
|
||||
[](https://codecov.io/gh/doctrine/instantiator/branch/master)
|
||||
[](https://www.versioneye.com/package/php--doctrine--instantiator)
|
||||
|
||||
[](https://packagist.org/packages/doctrine/instantiator)
|
||||
[](https://packagist.org/packages/doctrine/instantiator)
|
||||
|
||||
## Installation
|
||||
|
||||
The suggested installation method is via [composer](https://getcomposer.org/):
|
||||
|
||||
```sh
|
||||
composer require doctrine/instantiator
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The instantiator is able to create new instances of any class without using the constructor or any API of the class
|
||||
itself:
|
||||
|
||||
```php
|
||||
$instantiator = new \Doctrine\Instantiator\Instantiator();
|
||||
|
||||
$instance = $instantiator->instantiate(\My\ClassName\Here::class);
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read the [CONTRIBUTING.md](CONTRIBUTING.md) contents if you wish to help out!
|
||||
|
||||
## Credits
|
||||
|
||||
This library was migrated from [ocramius/instantiator](https://github.com/Ocramius/Instantiator), which
|
||||
has been donated to the doctrine organization, and which is now deprecated in favour of this package.
|
@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "doctrine/instantiator",
|
||||
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"homepage": "https://www.doctrine-project.org/projects/instantiator.html",
|
||||
"keywords": [
|
||||
"instantiate",
|
||||
"constructor"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Pivetta",
|
||||
"email": "ocramius@gmail.com",
|
||||
"homepage": "https://ocramius.github.io/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-phar": "*",
|
||||
"ext-pdo": "*",
|
||||
"doctrine/coding-standard": "^9 || ^11",
|
||||
"phpbench/phpbench": "^0.16 || ^1",
|
||||
"phpstan/phpstan": "^1.4",
|
||||
"phpstan/phpstan-phpunit": "^1",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
|
||||
"vimeo/psalm": "^4.30 || ^5.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-0": {
|
||||
"DoctrineTest\\InstantiatorPerformance\\": "tests",
|
||||
"DoctrineTest\\InstantiatorTest\\": "tests",
|
||||
"DoctrineTest\\InstantiatorTestAsset\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
index
|
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="7"
|
||||
phpVersion="8.2"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
</psalm>
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Instantiator\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base exception marker interface for the instantiator component
|
||||
*/
|
||||
interface ExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Instantiator\Exception;
|
||||
|
||||
use InvalidArgumentException as BaseInvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
|
||||
use function interface_exists;
|
||||
use function sprintf;
|
||||
use function trait_exists;
|
||||
|
||||
/**
|
||||
* Exception for invalid arguments provided to the instantiator
|
||||
*/
|
||||
class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
public static function fromNonExistingClass(string $className): self
|
||||
{
|
||||
if (interface_exists($className)) {
|
||||
return new self(sprintf('The provided type "%s" is an interface, and cannot be instantiated', $className));
|
||||
}
|
||||
|
||||
if (trait_exists($className)) {
|
||||
return new self(sprintf('The provided type "%s" is a trait, and cannot be instantiated', $className));
|
||||
}
|
||||
|
||||
return new self(sprintf('The provided class "%s" does not exist', $className));
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public static function fromAbstractClass(ReflectionClass $reflectionClass): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The provided class "%s" is abstract, and cannot be instantiated',
|
||||
$reflectionClass->getName()
|
||||
));
|
||||
}
|
||||
|
||||
public static function fromEnum(string $className): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The provided class "%s" is an enum, and cannot be instantiated',
|
||||
$className
|
||||
));
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Instantiator\Exception;
|
||||
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
use UnexpectedValueException as BaseUnexpectedValueException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Exception for given parameters causing invalid/unexpected state on instantiation
|
||||
*/
|
||||
class UnexpectedValueException extends BaseUnexpectedValueException implements ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public static function fromSerializationTriggeredException(
|
||||
ReflectionClass $reflectionClass,
|
||||
Exception $exception
|
||||
): self {
|
||||
return new self(
|
||||
sprintf(
|
||||
'An exception was raised while trying to instantiate an instance of "%s" via un-serialization',
|
||||
$reflectionClass->getName()
|
||||
),
|
||||
0,
|
||||
$exception
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public static function fromUncleanUnSerialization(
|
||||
ReflectionClass $reflectionClass,
|
||||
string $errorString,
|
||||
int $errorCode,
|
||||
string $errorFile,
|
||||
int $errorLine
|
||||
): self {
|
||||
return new self(
|
||||
sprintf(
|
||||
'Could not produce an instance of "%s" via un-serialization, since an error was triggered '
|
||||
. 'in file "%s" at line "%d"',
|
||||
$reflectionClass->getName(),
|
||||
$errorFile,
|
||||
$errorLine
|
||||
),
|
||||
0,
|
||||
new Exception($errorString, $errorCode)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Instantiator;
|
||||
|
||||
use ArrayIterator;
|
||||
use Doctrine\Instantiator\Exception\ExceptionInterface;
|
||||
use Doctrine\Instantiator\Exception\InvalidArgumentException;
|
||||
use Doctrine\Instantiator\Exception\UnexpectedValueException;
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use Serializable;
|
||||
|
||||
use function class_exists;
|
||||
use function enum_exists;
|
||||
use function is_subclass_of;
|
||||
use function restore_error_handler;
|
||||
use function set_error_handler;
|
||||
use function sprintf;
|
||||
use function strlen;
|
||||
use function unserialize;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
final class Instantiator implements InstantiatorInterface
|
||||
{
|
||||
/**
|
||||
* Markers used internally by PHP to define whether {@see \unserialize} should invoke
|
||||
* the method {@see \Serializable::unserialize()} when dealing with classes implementing
|
||||
* the {@see \Serializable} interface.
|
||||
*
|
||||
* @deprecated This constant will be private in 2.0
|
||||
*/
|
||||
public const SERIALIZATION_FORMAT_USE_UNSERIALIZER = 'C';
|
||||
|
||||
/** @deprecated This constant will be private in 2.0 */
|
||||
public const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
|
||||
|
||||
/**
|
||||
* Used to instantiate specific classes, indexed by class name.
|
||||
*
|
||||
* @var callable[]
|
||||
*/
|
||||
private static $cachedInstantiators = [];
|
||||
|
||||
/**
|
||||
* Array of objects that can directly be cloned, indexed by class name.
|
||||
*
|
||||
* @var object[]
|
||||
*/
|
||||
private static $cachedCloneables = [];
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
* @phpstan-param class-string<T> $className
|
||||
*
|
||||
* @return object
|
||||
* @phpstan-return T
|
||||
*
|
||||
* @throws ExceptionInterface
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public function instantiate($className)
|
||||
{
|
||||
if (isset(self::$cachedCloneables[$className])) {
|
||||
/** @phpstan-var T */
|
||||
$cachedCloneable = self::$cachedCloneables[$className];
|
||||
|
||||
return clone $cachedCloneable;
|
||||
}
|
||||
|
||||
if (isset(self::$cachedInstantiators[$className])) {
|
||||
$factory = self::$cachedInstantiators[$className];
|
||||
|
||||
return $factory();
|
||||
}
|
||||
|
||||
return $this->buildAndCacheFromFactory($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the requested object and caches it in static properties for performance
|
||||
*
|
||||
* @phpstan-param class-string<T> $className
|
||||
*
|
||||
* @return object
|
||||
* @phpstan-return T
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function buildAndCacheFromFactory(string $className)
|
||||
{
|
||||
$factory = self::$cachedInstantiators[$className] = $this->buildFactory($className);
|
||||
$instance = $factory();
|
||||
|
||||
if ($this->isSafeToClone(new ReflectionClass($instance))) {
|
||||
self::$cachedCloneables[$className] = clone $instance;
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a callable capable of instantiating the given $className without
|
||||
* invoking its constructor.
|
||||
*
|
||||
* @phpstan-param class-string<T> $className
|
||||
*
|
||||
* @phpstan-return callable(): T
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws UnexpectedValueException
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function buildFactory(string $className): callable
|
||||
{
|
||||
$reflectionClass = $this->getReflectionClass($className);
|
||||
|
||||
if ($this->isInstantiableViaReflection($reflectionClass)) {
|
||||
return [$reflectionClass, 'newInstanceWithoutConstructor'];
|
||||
}
|
||||
|
||||
$serializedString = sprintf(
|
||||
'%s:%d:"%s":0:{}',
|
||||
is_subclass_of($className, Serializable::class) ? self::SERIALIZATION_FORMAT_USE_UNSERIALIZER : self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
|
||||
strlen($className),
|
||||
$className
|
||||
);
|
||||
|
||||
$this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
|
||||
|
||||
return static function () use ($serializedString) {
|
||||
return unserialize($serializedString);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<T> $className
|
||||
*
|
||||
* @phpstan-return ReflectionClass<T>
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function getReflectionClass(string $className): ReflectionClass
|
||||
{
|
||||
if (! class_exists($className)) {
|
||||
throw InvalidArgumentException::fromNonExistingClass($className);
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID >= 80100 && enum_exists($className, false)) {
|
||||
throw InvalidArgumentException::fromEnum($className);
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
if ($reflection->isAbstract()) {
|
||||
throw InvalidArgumentException::fromAbstractClass($reflection);
|
||||
}
|
||||
|
||||
return $reflection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @throws UnexpectedValueException
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString): void
|
||||
{
|
||||
set_error_handler(static function (int $code, string $message, string $file, int $line) use ($reflectionClass, &$error): bool {
|
||||
$error = UnexpectedValueException::fromUncleanUnSerialization(
|
||||
$reflectionClass,
|
||||
$message,
|
||||
$code,
|
||||
$file,
|
||||
$line
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
$this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @throws UnexpectedValueException
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString): void
|
||||
{
|
||||
try {
|
||||
unserialize($serializedString);
|
||||
} catch (Exception $exception) {
|
||||
throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function isInstantiableViaReflection(ReflectionClass $reflectionClass): bool
|
||||
{
|
||||
return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the given class is to be considered internal
|
||||
*
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function hasInternalAncestors(ReflectionClass $reflectionClass): bool
|
||||
{
|
||||
do {
|
||||
if ($reflectionClass->isInternal()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$reflectionClass = $reflectionClass->getParentClass();
|
||||
} while ($reflectionClass);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a class is cloneable
|
||||
*
|
||||
* Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
|
||||
*
|
||||
* @phpstan-param ReflectionClass<T> $reflectionClass
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
private function isSafeToClone(ReflectionClass $reflectionClass): bool
|
||||
{
|
||||
return $reflectionClass->isCloneable()
|
||||
&& ! $reflectionClass->hasMethod('__clone')
|
||||
&& ! $reflectionClass->isSubclassOf(ArrayIterator::class);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Instantiator;
|
||||
|
||||
use Doctrine\Instantiator\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* Instantiator provides utility methods to build objects without invoking their constructors
|
||||
*/
|
||||
interface InstantiatorInterface
|
||||
{
|
||||
/**
|
||||
* @param string $className
|
||||
* @phpstan-param class-string<T> $className
|
||||
*
|
||||
* @return object
|
||||
* @phpstan-return T
|
||||
*
|
||||
* @throws ExceptionInterface
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public function instantiate($className);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-2023 Graham Campbell <hello@gjcampbell.co.uk>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"description": "An Implementation Of The Result Type",
|
||||
"keywords": ["result", "result-type", "Result", "Result Type", "Result-Type", "Graham Campbell", "GrahamCampbell"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GrahamCampbell\\ResultType\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"GrahamCampbell\\Tests\\ResultType\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist"
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Result Type.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace GrahamCampbell\ResultType;
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
* @extends \GrahamCampbell\ResultType\Result<T,E>
|
||||
*/
|
||||
final class Error extends Result
|
||||
{
|
||||
/**
|
||||
* @var E
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* Internal constructor for an error value.
|
||||
*
|
||||
* @param E $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param F $value
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
public static function create($value)
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \PhpOption\Option<T>
|
||||
*/
|
||||
public function success()
|
||||
{
|
||||
return None::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
public function map(callable $f)
|
||||
{
|
||||
return self::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,F>
|
||||
*/
|
||||
public function flatMap(callable $f)
|
||||
{
|
||||
/** @var \GrahamCampbell\ResultType\Result<S,F> */
|
||||
return self::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \PhpOption\Option<E>
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return Some::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
public function mapError(callable $f)
|
||||
{
|
||||
return self::create($f($this->value));
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Result Type.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace GrahamCampbell\ResultType;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*/
|
||||
abstract class Result
|
||||
{
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \PhpOption\Option<T>
|
||||
*/
|
||||
abstract public function success();
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
abstract public function map(callable $f);
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,F>
|
||||
*/
|
||||
abstract public function flatMap(callable $f);
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \PhpOption\Option<E>
|
||||
*/
|
||||
abstract public function error();
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
abstract public function mapError(callable $f);
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Result Type.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace GrahamCampbell\ResultType;
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
* @extends \GrahamCampbell\ResultType\Result<T,E>
|
||||
*/
|
||||
final class Success extends Result
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* Internal constructor for a success value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $value
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
public static function create($value)
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \PhpOption\Option<T>
|
||||
*/
|
||||
public function success()
|
||||
{
|
||||
return Some::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
public function map(callable $f)
|
||||
{
|
||||
return self::create($f($this->value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,F>
|
||||
*/
|
||||
public function flatMap(callable $f)
|
||||
{
|
||||
return $f($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \PhpOption\Option<E>
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return None::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
public function mapError(callable $f)
|
||||
{
|
||||
return self::create($this->value);
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"description": "Option Type for PHP",
|
||||
"keywords": ["php", "option", "language", "type"],
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Johannes M. Schmitt",
|
||||
"email": "schmittjoh@gmail.com",
|
||||
"homepage": "https://github.com/schmittjoh"
|
||||
},
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOption\\": "src/PhpOption/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"PhpOption\\Tests\\": "tests/PhpOption/Tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"bamarni/composer-bin-plugin": true
|
||||
},
|
||||
"preferred-install": "dist"
|
||||
},
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": true
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.9-dev"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace PhpOption;
|
||||
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Option<T>
|
||||
*/
|
||||
final class LazyOption extends Option
|
||||
{
|
||||
/** @var callable(mixed...):(Option<T>) */
|
||||
private $callback;
|
||||
|
||||
/** @var array<int, mixed> */
|
||||
private $arguments;
|
||||
|
||||
/** @var Option<T>|null */
|
||||
private $option;
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param callable(mixed...):(Option<S>) $callback
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return LazyOption<S>
|
||||
*/
|
||||
public static function create($callback, array $arguments = []): self
|
||||
{
|
||||
return new self($callback, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(mixed...):(Option<T>) $callback
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __construct($callback, array $arguments = [])
|
||||
{
|
||||
if (!is_callable($callback)) {
|
||||
throw new \InvalidArgumentException('Invalid callback given');
|
||||
}
|
||||
|
||||
$this->callback = $callback;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return $this->option()->isDefined();
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->option()->isEmpty();
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->option()->get();
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $this->option()->getOrElse($default);
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $this->option()->getOrCall($callable);
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
return $this->option()->getOrThrow($ex);
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $this->option()->orElse($else);
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
$this->option()->forAll($callable);
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
return $this->option()->forAll($callable);
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return $this->option()->map($callable);
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
return $this->option()->flatMap($callable);
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
return $this->option()->filter($callable);
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
return $this->option()->filterNot($callable);
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
return $this->option()->select($value);
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
return $this->option()->reject($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<T>
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->option()->getIterator();
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $this->option()->foldLeft($initialValue, $callable);
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $this->option()->foldRight($initialValue, $callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Option<T>
|
||||
*/
|
||||
private function option(): Option
|
||||
{
|
||||
if (null === $this->option) {
|
||||
/** @var mixed */
|
||||
$option = call_user_func_array($this->callback, $this->arguments);
|
||||
if ($option instanceof Option) {
|
||||
$this->option = $option;
|
||||
} else {
|
||||
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->option;
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace PhpOption;
|
||||
|
||||
use EmptyIterator;
|
||||
|
||||
/**
|
||||
* @extends Option<mixed>
|
||||
*/
|
||||
final class None extends Option
|
||||
{
|
||||
/** @var None|null */
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* @return None
|
||||
*/
|
||||
public static function create(): self
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
throw new \RuntimeException('None has no value.');
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $callable();
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $else;
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
// Just do nothing in that case.
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIterator(): EmptyIterator
|
||||
{
|
||||
return new EmptyIterator();
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $initialValue;
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $initialValue;
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
@ -1,434 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace PhpOption;
|
||||
|
||||
use ArrayAccess;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @implements IteratorAggregate<T>
|
||||
*/
|
||||
abstract class Option implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* Creates an option given a return value.
|
||||
*
|
||||
* This is intended for consuming existing APIs and allows you to easily
|
||||
* convert them to an option. By default, we treat ``null`` as the None
|
||||
* case, and everything else as Some.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $value The actual return value.
|
||||
* @param S $noneValue The value which should be considered "None"; null by
|
||||
* default.
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
public static function fromValue($value, $noneValue = null)
|
||||
{
|
||||
if ($value === $noneValue) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an option from an array's value.
|
||||
*
|
||||
* If the key does not exist in the array, the array is not actually an
|
||||
* array, or the array's value at the given key is null, None is returned.
|
||||
* Otherwise, Some is returned wrapping the value at the given key.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array A potential array or \ArrayAccess value.
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
public static function fromArraysValue($array, $key)
|
||||
{
|
||||
if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($array[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy-option with the given callback.
|
||||
*
|
||||
* This is also a helper constructor for lazy-consuming existing APIs where
|
||||
* the return value is not yet an option. By default, we treat ``null`` as
|
||||
* None case, and everything else as Some.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable $callback The callback to evaluate.
|
||||
* @param array $arguments The arguments for the callback.
|
||||
* @param S $noneValue The value which should be considered "None";
|
||||
* null by default.
|
||||
*
|
||||
* @return LazyOption<S>
|
||||
*/
|
||||
public static function fromReturn($callback, array $arguments = [], $noneValue = null)
|
||||
{
|
||||
return new LazyOption(static function () use ($callback, $arguments, $noneValue) {
|
||||
/** @var mixed */
|
||||
$return = call_user_func_array($callback, $arguments);
|
||||
|
||||
if ($return === $noneValue) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($return);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Option factory, which creates new option based on passed value.
|
||||
*
|
||||
* If value is already an option, it simply returns. If value is callable,
|
||||
* LazyOption with passed callback created and returned. If Option
|
||||
* returned from callback, it returns directly. On other case value passed
|
||||
* to Option::fromValue() method.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param Option<S>|callable|S $value
|
||||
* @param S $noneValue Used when $value is mixed or
|
||||
* callable, for None-check.
|
||||
*
|
||||
* @return Option<S>|LazyOption<S>
|
||||
*/
|
||||
public static function ensure($value, $noneValue = null)
|
||||
{
|
||||
if ($value instanceof self) {
|
||||
return $value;
|
||||
} elseif (is_callable($value)) {
|
||||
return new LazyOption(static function () use ($value, $noneValue) {
|
||||
/** @var mixed */
|
||||
$return = $value();
|
||||
|
||||
if ($return instanceof self) {
|
||||
return $return;
|
||||
} else {
|
||||
return self::fromValue($return, $noneValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self::fromValue($value, $noneValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lift a function so that it accepts Option as parameters.
|
||||
*
|
||||
* We return a new closure that wraps the original callback. If any of the
|
||||
* parameters passed to the lifted function is empty, the function will
|
||||
* return a value of None. Otherwise, we will pass all parameters to the
|
||||
* original callback and return the value inside a new Option, unless an
|
||||
* Option is returned from the function, in which case, we use that.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param mixed $noneValue
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public static function lift($callback, $noneValue = null)
|
||||
{
|
||||
return static function () use ($callback, $noneValue) {
|
||||
/** @var array<int, mixed> */
|
||||
$args = func_get_args();
|
||||
|
||||
$reduced_args = array_reduce(
|
||||
$args,
|
||||
/** @param bool $status */
|
||||
static function ($status, self $o) {
|
||||
return $o->isEmpty() ? true : $status;
|
||||
},
|
||||
false
|
||||
);
|
||||
// if at least one parameter is empty, return None
|
||||
if ($reduced_args) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
$args = array_map(
|
||||
/** @return T */
|
||||
static function (self $o) {
|
||||
// it is safe to do so because the fold above checked
|
||||
// that all arguments are of type Some
|
||||
/** @var T */
|
||||
return $o->get();
|
||||
},
|
||||
$args
|
||||
);
|
||||
|
||||
return self::ensure(call_user_func_array($callback, $args), $noneValue);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value if available, or throws an exception otherwise.
|
||||
*
|
||||
* @throws \RuntimeException If value is not available.
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
abstract public function get();
|
||||
|
||||
/**
|
||||
* Returns the value if available, or the default value if not.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $default
|
||||
*
|
||||
* @return T|S
|
||||
*/
|
||||
abstract public function getOrElse($default);
|
||||
|
||||
/**
|
||||
* Returns the value if available, or the results of the callable.
|
||||
*
|
||||
* This is preferable over ``getOrElse`` if the computation of the default
|
||||
* value is expensive.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable():S $callable
|
||||
*
|
||||
* @return T|S
|
||||
*/
|
||||
abstract public function getOrCall($callable);
|
||||
|
||||
/**
|
||||
* Returns the value if available, or throws the passed exception.
|
||||
*
|
||||
* @param \Exception $ex
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
abstract public function getOrThrow(\Exception $ex);
|
||||
|
||||
/**
|
||||
* Returns true if no value is available, false otherwise.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isEmpty();
|
||||
|
||||
/**
|
||||
* Returns true if a value is available, false otherwise.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isDefined();
|
||||
|
||||
/**
|
||||
* Returns this option if non-empty, or the passed option otherwise.
|
||||
*
|
||||
* This can be used to try multiple alternatives, and is especially useful
|
||||
* with lazy evaluating options:
|
||||
*
|
||||
* ```php
|
||||
* $repo->findSomething()
|
||||
* ->orElse(new LazyOption(array($repo, 'findSomethingElse')))
|
||||
* ->orElse(new LazyOption(array($repo, 'createSomething')));
|
||||
* ```
|
||||
*
|
||||
* @param Option<T> $else
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function orElse(self $else);
|
||||
|
||||
/**
|
||||
* This is similar to map() below except that the return value has no meaning;
|
||||
* the passed callable is simply executed if the option is non-empty, and
|
||||
* ignored if the option is empty.
|
||||
*
|
||||
* In all cases, the return value of the callable is discarded.
|
||||
*
|
||||
* ```php
|
||||
* $comment->getMaybeFile()->ifDefined(function($file) {
|
||||
* // Do something with $file here.
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If you're looking for something like ``ifEmpty``, you can use ``getOrCall``
|
||||
* and ``getOrElse`` in these cases.
|
||||
*
|
||||
* @deprecated Use forAll() instead.
|
||||
*
|
||||
* @param callable(T):mixed $callable
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function ifDefined($callable);
|
||||
|
||||
/**
|
||||
* This is similar to map() except that the return value of the callable has no meaning.
|
||||
*
|
||||
* The passed callable is simply executed if the option is non-empty, and ignored if the
|
||||
* option is empty. This method is preferred for callables with side-effects, while map()
|
||||
* is intended for callables without side-effects.
|
||||
*
|
||||
* @param callable(T):mixed $callable
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function forAll($callable);
|
||||
|
||||
/**
|
||||
* Applies the callable to the value of the option if it is non-empty,
|
||||
* and returns the return value of the callable wrapped in Some().
|
||||
*
|
||||
* If the option is empty, then the callable is not applied.
|
||||
*
|
||||
* ```php
|
||||
* (new Some("foo"))->map('strtoupper')->get(); // "FOO"
|
||||
* ```
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $callable
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
abstract public function map($callable);
|
||||
|
||||
/**
|
||||
* Applies the callable to the value of the option if it is non-empty, and
|
||||
* returns the return value of the callable directly.
|
||||
*
|
||||
* In contrast to ``map``, the return value of the callable is expected to
|
||||
* be an Option itself; it is not automatically wrapped in Some().
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):Option<S> $callable must return an Option
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
abstract public function flatMap($callable);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately without applying the callable.
|
||||
*
|
||||
* If the option is non-empty, the callable is applied, and if it returns true,
|
||||
* the option itself is returned; otherwise, None is returned.
|
||||
*
|
||||
* @param callable(T):bool $callable
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function filter($callable);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately without applying the callable.
|
||||
*
|
||||
* If the option is non-empty, the callable is applied, and if it returns false,
|
||||
* the option itself is returned; otherwise, None is returned.
|
||||
*
|
||||
* @param callable(T):bool $callable
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function filterNot($callable);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately.
|
||||
*
|
||||
* If the option is non-empty, and its value does not equal the passed value
|
||||
* (via a shallow comparison ===), then None is returned. Otherwise, the
|
||||
* Option is returned.
|
||||
*
|
||||
* In other words, this will filter all but the passed value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function select($value);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately.
|
||||
*
|
||||
* If the option is non-empty, and its value does equal the passed value (via
|
||||
* a shallow comparison ===), then None is returned; otherwise, the Option is
|
||||
* returned.
|
||||
*
|
||||
* In other words, this will let all values through except the passed value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function reject($value);
|
||||
|
||||
/**
|
||||
* Binary operator for the initial value and the option's value.
|
||||
*
|
||||
* If empty, the initial value is returned. If non-empty, the callable
|
||||
* receives the initial value and the option's value as arguments.
|
||||
*
|
||||
* ```php
|
||||
*
|
||||
* $some = new Some(5);
|
||||
* $none = None::create();
|
||||
* $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6)
|
||||
* $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1)
|
||||
*
|
||||
* // This can be used instead of something like the following:
|
||||
* $option = Option::fromValue($integerOrNull);
|
||||
* $result = 1;
|
||||
* if ( ! $option->isEmpty()) {
|
||||
* $result += $option->get();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $initialValue
|
||||
* @param callable(S, T):S $callable
|
||||
*
|
||||
* @return S
|
||||
*/
|
||||
abstract public function foldLeft($initialValue, $callable);
|
||||
|
||||
/**
|
||||
* foldLeft() but with reversed arguments for the callable.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $initialValue
|
||||
* @param callable(T, S):S $callable
|
||||
*
|
||||
* @return S
|
||||
*/
|
||||
abstract public function foldRight($initialValue, $callable);
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace PhpOption;
|
||||
|
||||
use ArrayIterator;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Option<T>
|
||||
*/
|
||||
final class Some extends Option
|
||||
{
|
||||
/** @var T */
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template U
|
||||
*
|
||||
* @param U $value
|
||||
*
|
||||
* @return Some<U>
|
||||
*/
|
||||
public static function create($value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
$this->forAll($callable);
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
$callable($this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return new self($callable($this->value));
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
/** @var mixed */
|
||||
$rs = $callable($this->value);
|
||||
if (!$rs instanceof Option) {
|
||||
throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?');
|
||||
}
|
||||
|
||||
return $rs;
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
if (true === $callable($this->value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
if (false === $callable($this->value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
if ($this->value === $value) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
if ($this->value === $value) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayIterator<int, T>
|
||||
*/
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
return new ArrayIterator([$this->value]);
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $callable($initialValue, $this->value);
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $callable($this->value, $initialValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
# ChangeLog
|
||||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
|
||||
|
||||
## [10.1.9] - 2023-11-23
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1020](https://github.com/sebastianbergmann/php-code-coverage/issues/1020): Single line method is ignored
|
||||
|
||||
## [10.1.8] - 2023-11-15
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1018](https://github.com/sebastianbergmann/php-code-coverage/issues/1018): Interface methods are not ignored when their signature is split over multiple lines
|
||||
|
||||
## [10.1.7] - 2023-10-04
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1014](https://github.com/sebastianbergmann/php-code-coverage/issues/1014): Incorrect statement count in coverage report for constructor property promotion
|
||||
|
||||
## [10.1.6] - 2023-09-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
|
||||
|
||||
## [10.1.5] - 2023-09-12
|
||||
|
||||
### Changed
|
||||
|
||||
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
|
||||
|
||||
## [10.1.4] - 2023-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
* Exceptions of type `SebastianBergmann\Template\Exception` are now properly handled
|
||||
|
||||
## [10.1.3] - 2023-07-26
|
||||
|
||||
### Changed
|
||||
|
||||
* The result of `CodeCoverage::getReport()` is now cached
|
||||
|
||||
### Fixed
|
||||
|
||||
* Static analysis cache keys do not include configuration settings that affect source code parsing
|
||||
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
|
||||
|
||||
## [10.1.2] - 2023-05-22
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#998](https://github.com/sebastianbergmann/php-code-coverage/pull/998): Group Use Declarations are not handled properly
|
||||
|
||||
## [10.1.1] - 2023-04-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#994](https://github.com/sebastianbergmann/php-code-coverage/issues/994): Argument `$linesToBeIgnored` of `CodeCoverage::stop()` has no effect for files that are not executed at all
|
||||
|
||||
## [10.1.0] - 2023-04-13
|
||||
|
||||
### Added
|
||||
|
||||
* [#982](https://github.com/sebastianbergmann/php-code-coverage/issues/982): Add option to ignore lines from code coverage
|
||||
|
||||
### Deprecated
|
||||
|
||||
* The `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`, `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods are now deprecated
|
||||
|
||||
[10.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.8...10.1.9
|
||||
[10.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.7...10.1.8
|
||||
[10.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.6...10.1.7
|
||||
[10.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.5...10.1.6
|
||||
[10.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.4...10.1.5
|
||||
[10.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.3...10.1.4
|
||||
[10.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.2...10.1.3
|
||||
[10.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.1...10.1.2
|
||||
[10.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.0...10.1.1
|
||||
[10.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.0.2...10.1.0
|
@ -1,563 +0,0 @@
|
||||
# ChangeLog
|
||||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
|
||||
|
||||
## [9.2.29] - 2023-09-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
|
||||
|
||||
## [9.2.28] - 2023-09-12
|
||||
|
||||
### Changed
|
||||
|
||||
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
|
||||
|
||||
## [9.2.27] - 2023-07-26
|
||||
|
||||
### Changed
|
||||
|
||||
* The result of `CodeCoverage::getReport()` is now cached
|
||||
|
||||
### Fixed
|
||||
|
||||
* Static analysis cache keys do not include configuration settings that affect source code parsing
|
||||
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
|
||||
|
||||
## [9.2.26] - 2023-03-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Improved the legend on the file pages of the HTML code coverage report
|
||||
|
||||
## [9.2.25] - 2023-02-25
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#981](https://github.com/sebastianbergmann/php-code-coverage/issues/981): `CodeUnitFindingVisitor` does not support DNF types
|
||||
|
||||
## [9.2.24] - 2023-01-26
|
||||
|
||||
### Changed
|
||||
|
||||
* [#970](https://github.com/sebastianbergmann/php-code-coverage/issues/970): CSS and JavaScript assets are now referenced using `?v=%s` URLs in the HTML report to avoid cache issues
|
||||
|
||||
## [9.2.23] - 2022-12-28
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#971](https://github.com/sebastianbergmann/php-code-coverage/issues/971): PHP report does not handle serialized code coverage data larger than 2 GB
|
||||
* [#974](https://github.com/sebastianbergmann/php-code-coverage/issues/974): Executable line analysis fails for declarations with enumerations and unions
|
||||
|
||||
## [9.2.22] - 2022-12-18
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#969](https://github.com/sebastianbergmann/php-code-coverage/pull/969): Fixed identifying line with `throw` as executable
|
||||
|
||||
## [9.2.21] - 2022-12-14
|
||||
|
||||
### Changed
|
||||
|
||||
* [#964](https://github.com/sebastianbergmann/php-code-coverage/pull/964): Changed how executable lines are identified
|
||||
|
||||
## [9.2.20] - 2022-12-13
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#960](https://github.com/sebastianbergmann/php-code-coverage/issues/960): New body font-size is way too big
|
||||
|
||||
## [9.2.19] - 2022-11-18
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#949](https://github.com/sebastianbergmann/php-code-coverage/pull/949): Various issues related to identifying executable lines
|
||||
|
||||
### Changed
|
||||
|
||||
* Tweaked CSS for HTML report
|
||||
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.2 and jQuery 3.6.1
|
||||
|
||||
## [9.2.18] - 2022-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#935](https://github.com/sebastianbergmann/php-code-coverage/pull/935): Cobertura package name attribute is always empty
|
||||
* [#946](https://github.com/sebastianbergmann/php-code-coverage/issues/946): `return` with multiline constant expression must only contain the last line
|
||||
|
||||
## [9.2.17] - 2022-08-30
|
||||
|
||||
### Changed
|
||||
|
||||
* [#928](https://github.com/sebastianbergmann/php-code-coverage/pull/928): Avoid unnecessary `is_file()` calls
|
||||
* [#931](https://github.com/sebastianbergmann/php-code-coverage/pull/931): Use MD5 instead of CRC32 for static analysis cache file identifier
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#926](https://github.com/sebastianbergmann/php-code-coverage/pull/926): Static Analysis cache does not work with `open_basedir`
|
||||
|
||||
## [9.2.16] - 2022-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#926](https://github.com/sebastianbergmann/php-code-coverage/issues/926): File view has wrong colouring for the first column
|
||||
|
||||
## [9.2.15] - 2022-03-07
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#885](https://github.com/sebastianbergmann/php-code-coverage/issues/885): Files that have only `\r` (CR, 0x0d) EOL characters are not handled correctly
|
||||
* [#907](https://github.com/sebastianbergmann/php-code-coverage/issues/907): Line with only `return [` is not recognized as executable
|
||||
|
||||
## [9.2.14] - 2022-02-28
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#904](https://github.com/sebastianbergmann/php-code-coverage/issues/904): Lines of code containing the `match` keyword were not recognized as executable correctly
|
||||
* [#905](https://github.com/sebastianbergmann/php-code-coverage/issues/905): Lines of code in constructors were not recognized as executable correctly when constructor property promotion is used
|
||||
|
||||
## [9.2.13] - 2022-02-23
|
||||
|
||||
### Changed
|
||||
|
||||
* The contents of the static analysis sourcecode files is now used to generate the static analysis cache version identifier
|
||||
|
||||
### Fixed
|
||||
|
||||
* Reverted rename of `SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData` to `SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData` (this class is marked as `@internal` and not covered by the backward compatibility promise, but it is (still) used directly by PHPUnit)
|
||||
* Reverted rename of `SebastianBergmann\CodeCoverage\RawCodeCoverageData` to `SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData` (this class is marked as `@internal` and not covered by the backward compatibility promise, but it is (still) used directly by PHPUnit)
|
||||
* The `ArrayDim`, `Cast`, and `MethodCall` nodes are now considered when determining whether a line of code is executable or not
|
||||
|
||||
## [9.2.12] - 2022-02-23 [YANKED]
|
||||
|
||||
### Changed
|
||||
|
||||
* [#898](https://github.com/sebastianbergmann/php-code-coverage/pull/898): Use content hash instead of `filemtime()` to determine cache hit/miss
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#736](https://github.com/sebastianbergmann/php-code-coverage/issues/736): HTML report generator allows invalid values for low upper bound and high lower bound
|
||||
* [#854](https://github.com/sebastianbergmann/php-code-coverage/issues/854): "Class Coverage Distribution" and "Class Complexity" graphs are not displayed at full width
|
||||
* [#897](https://github.com/sebastianbergmann/php-code-coverage/issues/897): `declare(strict_types=1)` marked as uncovered
|
||||
|
||||
## [9.2.11] - 2022-02-18
|
||||
|
||||
### Changed
|
||||
|
||||
* `CoveredFileAnalyser` and `UncoveredFileAnalyser` have been combined to `FileAnalyser`
|
||||
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.1, jQuery 3.6.0, and popper.js 1.16.1
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#889](https://github.com/sebastianbergmann/php-code-coverage/issues/889): Code Coverage depends on autoload order
|
||||
|
||||
## [9.2.10] - 2021-12-05
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#887](https://github.com/sebastianbergmann/php-code-coverage/issues/887): Document return type of `CodeUnitFindingVisitor::enterNode()` so that Symfony's DebugClassLoader does not trigger a deprecation warning
|
||||
|
||||
## [9.2.9] - 2021-11-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#882](https://github.com/sebastianbergmann/php-code-coverage/issues/882): PHPUnit 9.2.8 has wrong version number
|
||||
|
||||
## [9.2.8] - 2021-10-30
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#866](https://github.com/sebastianbergmann/php-code-coverage/issues/866): `CodeUnitFindingVisitor` does not handle `enum` type introduced in PHP 8.1
|
||||
* [#868](https://github.com/sebastianbergmann/php-code-coverage/pull/868): Uncovered files should be ignored unless requested
|
||||
* [#876](https://github.com/sebastianbergmann/php-code-coverage/issues/876): PCOV driver causes 2x slowdown after upgrade to PHPUnit 9.5
|
||||
|
||||
## [9.2.7] - 2021-09-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#860](https://github.com/sebastianbergmann/php-code-coverage/pull/860): Empty value for `XDEBUG_MODE` environment variable is not handled correctly
|
||||
|
||||
## [9.2.6] - 2021-03-28
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#846](https://github.com/sebastianbergmann/php-code-coverage/issues/846): Method name should not appear in the method signature attribute of Cobertura XML
|
||||
|
||||
## [9.2.5] - 2020-11-28
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#831](https://github.com/sebastianbergmann/php-code-coverage/issues/831): Files that do not contain a newline are not handled correctly
|
||||
|
||||
## [9.2.4] - 2020-11-27
|
||||
|
||||
### Added
|
||||
|
||||
* [#834](https://github.com/sebastianbergmann/php-code-coverage/issues/834): Support `XDEBUG_MODE` environment variable
|
||||
|
||||
## [9.2.3] - 2020-10-30
|
||||
|
||||
### Changed
|
||||
|
||||
* Bumped required version of `nikic/php-parser`
|
||||
|
||||
## [9.2.2] - 2020-10-28
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#820](https://github.com/sebastianbergmann/php-code-coverage/issues/820): Hidden dependency on PHPUnit
|
||||
|
||||
## [9.2.1] - 2020-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
* `SebastianBergmann\CodeCoverage\Exception` now correctly extends `\Throwable`
|
||||
|
||||
## [9.2.0] - 2020-10-02
|
||||
|
||||
### Added
|
||||
|
||||
* [#812](https://github.com/sebastianbergmann/php-code-coverage/pull/812): Support for Cobertura XML report format
|
||||
|
||||
### Changed
|
||||
|
||||
* Reduced the number of I/O operations performed by the static analysis cache
|
||||
|
||||
## [9.1.11] - 2020-09-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#811](https://github.com/sebastianbergmann/php-code-coverage/issues/811): `T_FN` constant is used on PHP 7.3 where it is not available
|
||||
|
||||
## [9.1.10] - 2020-09-18
|
||||
|
||||
### Added
|
||||
|
||||
* `SebastianBergmann\CodeCoverage\Driver\Selector::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Selector::forLineAndPathCoverage()` have been added
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#810](https://github.com/sebastianbergmann/php-code-coverage/issues/810): `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` are marked as internal
|
||||
|
||||
### Removed
|
||||
|
||||
* `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` are now deprecated
|
||||
|
||||
## [9.1.9] - 2020-09-15
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#808](https://github.com/sebastianbergmann/php-code-coverage/issues/808): `PHP Warning: Use of undefined constant T_MATCH`
|
||||
|
||||
## [9.1.8] - 2020-09-07
|
||||
|
||||
### Changed
|
||||
|
||||
* [#800](https://github.com/sebastianbergmann/php-code-coverage/pull/800): All files on the inclusion list are no longer loaded when `SebastianBergmann\CodeCoverage::start()` is called for the first time and `processUncoveredFiles` is set to `true`
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#799](https://github.com/sebastianbergmann/php-code-coverage/issues/799): Uncovered new line at end of file
|
||||
|
||||
## [9.1.7] - 2020-09-03
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed regressions introduced in versions 9.1.5 and 9.1.6
|
||||
|
||||
## [9.1.6] - 2020-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#799](https://github.com/sebastianbergmann/php-code-coverage/issues/799): Uncovered new line at end of file
|
||||
* [#803](https://github.com/sebastianbergmann/php-code-coverage/issues/803): HTML report does not sort directories and files anymore
|
||||
|
||||
## [9.1.5] - 2020-08-27
|
||||
|
||||
### Changed
|
||||
|
||||
* [#800](https://github.com/sebastianbergmann/php-code-coverage/pull/800): All files on the inclusion list are no longer loaded when `SebastianBergmann\CodeCoverage::start()` is called for the first time and `processUncoveredFiles` is set to `true`
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#797](https://github.com/sebastianbergmann/php-code-coverage/pull/797): Class name is wrongly removed from namespace name
|
||||
|
||||
## [9.1.4] - 2020-08-13
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#793](https://github.com/sebastianbergmann/php-code-coverage/issues/793): Lines with `::class` constant are not covered
|
||||
|
||||
## [9.1.3] - 2020-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Changed PHP-Parser usage to parse sourcecode according to the PHP version we are currently running on instead of using emulative lexing
|
||||
|
||||
## [9.1.2] - 2020-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#791](https://github.com/sebastianbergmann/php-code-coverage/pull/791): Cache Warmer does not warm all caches
|
||||
|
||||
## [9.1.1] - 2020-08-10
|
||||
|
||||
### Added
|
||||
|
||||
* Added `SebastianBergmann\CodeCoverage::cacheDirectory()` method for querying where the cache writes its files
|
||||
|
||||
## [9.1.0] - 2020-08-10
|
||||
|
||||
### Added
|
||||
|
||||
* Implemented a persistent cache for information gathered using PHP-Parser based static analysis (hereinafter referred to as "cache")
|
||||
* Added `SebastianBergmann\CodeCoverage::cacheStaticAnalysis(string $cacheDirectory)` method for enabling the cache; it will write its files to `$directory`
|
||||
* Added `SebastianBergmann\CodeCoverage::doNotCacheStaticAnalysis` method for disabling the cache
|
||||
* Added `SebastianBergmann\CodeCoverage::cachesStaticAnalysis()` method for querying whether the cache is enabled
|
||||
* Added `SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer::warmCache()` method for warming the cache
|
||||
|
||||
## [9.0.0] - 2020-08-07
|
||||
|
||||
### Added
|
||||
|
||||
* [#761](https://github.com/sebastianbergmann/php-code-coverage/pull/761): Support for Branch Coverage and Path Coverage
|
||||
* Added `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` for selecting the best available driver for line coverage
|
||||
* Added `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` for selecting the best available driver for path coverage
|
||||
* This component is now supported on PHP 8
|
||||
* This component now supports Xdebug 3
|
||||
|
||||
### Changed
|
||||
|
||||
* [#746](https://github.com/sebastianbergmann/php-code-coverage/pull/746): Remove some ancient workarounds for very old Xdebug versions
|
||||
* [#747](https://github.com/sebastianbergmann/php-code-coverage/pull/747): Use native filtering in PCOV and Xdebug drivers
|
||||
* [#748](https://github.com/sebastianbergmann/php-code-coverage/pull/748): Store raw code coverage in value objects instead of arrays
|
||||
* [#749](https://github.com/sebastianbergmann/php-code-coverage/pull/749): Store processed code coverage in value objects instead of arrays
|
||||
* [#752](https://github.com/sebastianbergmann/php-code-coverage/pull/752): Rework how code coverage settings are propagated to the driver
|
||||
* [#754](https://github.com/sebastianbergmann/php-code-coverage/pull/754): Implement collection of raw branch and path coverage
|
||||
* [#755](https://github.com/sebastianbergmann/php-code-coverage/pull/755): Implement processing of raw branch and path coverage
|
||||
* [#756](https://github.com/sebastianbergmann/php-code-coverage/pull/756): Improve handling of uncovered files
|
||||
* `SebastianBergmann\CodeCoverage\Filter::addDirectoryToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`
|
||||
* `SebastianBergmann\CodeCoverage\Filter::addFilesToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeFiles()`
|
||||
* `SebastianBergmann\CodeCoverage\Filter::addFileToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeFile()`
|
||||
* `SebastianBergmann\CodeCoverage\Filter::removeDirectoryFromWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`
|
||||
* `SebastianBergmann\CodeCoverage\Filter::removeFileFromWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::excludeFile()`
|
||||
* `SebastianBergmann\CodeCoverage\Filter::isFiltered()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::isExcluded()`
|
||||
* `SebastianBergmann\CodeCoverage\Filter::getWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::files()`
|
||||
* The arguments for `CodeCoverage::__construct()` are no longer optional
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#700](https://github.com/sebastianbergmann/php-code-coverage/pull/700): Throw an exception if code coverage fails to write to disk
|
||||
|
||||
### Removed
|
||||
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCacheTokens()` and `SebastianBergmann\CodeCoverage\CodeCoverage::getCacheTokens()` have been removed
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForUnintentionallyCoveredCode()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::enableCheckForUnintentionallyCoveredCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::disableCheckForUnintentionallyCoveredCode()` instead
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setSubclassesExcludedFromUnintentionallyCoveredCodeCheck()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck()` instead
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setAddUncoveredFilesFromWhitelist()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::includeUncoveredFiles()` or `SebastianBergmann\CodeCoverage\CodeCoverage::excludeUncoveredFiles()` instead
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setProcessUncoveredFiles()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::processUncoveredFiles()` or `SebastianBergmann\CodeCoverage\CodeCoverage::doNotProcessUncoveredFiles()` instead
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setIgnoreDeprecatedCode()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::ignoreDeprecatedCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::doNotIgnoreDeprecatedCode()` instead
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setDisableIgnoredLines()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::enableAnnotationsForIgnoringCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::disableAnnotationsForIgnoringCode()` instead
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForMissingCoversAnnotation()` has been removed
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForUnexecutedCoveredCode()` has been removed
|
||||
* `SebastianBergmann\CodeCoverage\CodeCoverage::setForceCoversAnnotation()` has been removed
|
||||
* `SebastianBergmann\CodeCoverage\Filter::hasWhitelist()` has been removed, please use `SebastianBergmann\CodeCoverage\Filter::isEmpty()` instead
|
||||
* `SebastianBergmann\CodeCoverage\Filter::getWhitelistedFiles()` has been removed
|
||||
* `SebastianBergmann\CodeCoverage\Filter::setWhitelistedFiles()` has been removed
|
||||
|
||||
## [8.0.2] - 2020-05-23
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#750](https://github.com/sebastianbergmann/php-code-coverage/pull/750): Inconsistent handling of namespaces
|
||||
* [#751](https://github.com/sebastianbergmann/php-code-coverage/pull/751): Dead code is not highlighted correctly
|
||||
* [#753](https://github.com/sebastianbergmann/php-code-coverage/issues/753): Do not use `$_SERVER['REQUEST_TIME']` because the test(ed) code might unset it
|
||||
|
||||
## [8.0.1] - 2020-02-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#731](https://github.com/sebastianbergmann/php-code-coverage/pull/731): Confusing footer in the HTML report
|
||||
|
||||
## [8.0.0] - 2020-02-07
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#721](https://github.com/sebastianbergmann/php-code-coverage/pull/721): Workaround for PHP bug [#79191](https://bugs.php.net/bug.php?id=79191)
|
||||
|
||||
### Removed
|
||||
|
||||
* This component is no longer supported on PHP 7.2
|
||||
|
||||
## [7.0.15] - 2021-07-26
|
||||
|
||||
### Changed
|
||||
|
||||
* Bumped required version of php-token-stream
|
||||
|
||||
## [7.0.14] - 2020-12-02
|
||||
|
||||
### Changed
|
||||
|
||||
* [#837](https://github.com/sebastianbergmann/php-code-coverage/issues/837): Allow version 4 of php-token-stream
|
||||
|
||||
## [7.0.13] - 2020-11-30
|
||||
|
||||
### Changed
|
||||
|
||||
* Changed PHP version constraint in `composer.json` from `^7.2` to `>=7.2` to allow installation of this version of this library on PHP 8. However, this version of this library does not work on PHP 8. PHPUnit 8.5, which uses this version of this library, does not call into this library and instead shows a message that code coverage functionality is not available for PHPUnit 8.5 on PHP 8.
|
||||
|
||||
## [7.0.12] - 2020-11-27
|
||||
|
||||
### Added
|
||||
|
||||
* [#834](https://github.com/sebastianbergmann/php-code-coverage/issues/834): Support `XDEBUG_MODE` environment variable
|
||||
|
||||
## [7.0.11] - 2020-11-27
|
||||
|
||||
### Added
|
||||
|
||||
* Support for Xdebug 3
|
||||
|
||||
## [7.0.10] - 2019-11-20
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#710](https://github.com/sebastianbergmann/php-code-coverage/pull/710): Code Coverage does not work in PhpStorm
|
||||
|
||||
## [7.0.9] - 2019-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
* [#709](https://github.com/sebastianbergmann/php-code-coverage/pull/709): Prioritize PCOV over Xdebug
|
||||
|
||||
## [7.0.8] - 2019-09-17
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.3.1, jQuery 3.4.1, and popper.js 1.15.0
|
||||
|
||||
## [7.0.7] - 2019-07-25
|
||||
|
||||
### Changed
|
||||
|
||||
* Bumped required version of php-token-stream
|
||||
|
||||
## [7.0.6] - 2019-07-08
|
||||
|
||||
### Changed
|
||||
|
||||
* Bumped required version of php-token-stream
|
||||
|
||||
## [7.0.5] - 2019-06-06
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#681](https://github.com/sebastianbergmann/php-code-coverage/pull/681): `use function` statements are not ignored
|
||||
|
||||
## [7.0.4] - 2019-05-29
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#682](https://github.com/sebastianbergmann/php-code-coverage/pull/682): Code that is not executed is reported as being executed when using PCOV
|
||||
|
||||
## [7.0.3] - 2019-02-26
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#671](https://github.com/sebastianbergmann/php-code-coverage/issues/671): `TypeError` when directory name is a number
|
||||
|
||||
## [7.0.2] - 2019-02-15
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.3.0
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#667](https://github.com/sebastianbergmann/php-code-coverage/pull/667): `TypeError` in PHP reporter
|
||||
|
||||
## [7.0.1] - 2019-02-01
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#664](https://github.com/sebastianbergmann/php-code-coverage/issues/664): `TypeError` when whitelisted file does not exist
|
||||
|
||||
## [7.0.0] - 2019-02-01
|
||||
|
||||
### Added
|
||||
|
||||
* [#663](https://github.com/sebastianbergmann/php-code-coverage/pull/663): Support for PCOV
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#654](https://github.com/sebastianbergmann/php-code-coverage/issues/654): HTML report fails to load assets
|
||||
* [#655](https://github.com/sebastianbergmann/php-code-coverage/issues/655): Popin pops in outside of screen
|
||||
|
||||
### Removed
|
||||
|
||||
* This component is no longer supported on PHP 7.1
|
||||
|
||||
[9.2.29]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.28...9.2.29
|
||||
[9.2.28]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.27...9.2.28
|
||||
[9.2.27]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.26...9.2.27
|
||||
[9.2.26]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.25...9.2.26
|
||||
[9.2.25]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.24...9.2.25
|
||||
[9.2.24]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.23...9.2.24
|
||||
[9.2.23]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.22...9.2.23
|
||||
[9.2.22]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.21...9.2.22
|
||||
[9.2.21]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.20...9.2.21
|
||||
[9.2.20]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.19...9.2.20
|
||||
[9.2.19]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.18...9.2.19
|
||||
[9.2.18]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.17...9.2.18
|
||||
[9.2.17]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.16...9.2.17
|
||||
[9.2.16]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.15...9.2.16
|
||||
[9.2.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.14...9.2.15
|
||||
[9.2.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.13...9.2.14
|
||||
[9.2.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631...9.2.13
|
||||
[9.2.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.11...c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631
|
||||
[9.2.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.10...9.2.11
|
||||
[9.2.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.9...9.2.10
|
||||
[9.2.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.8...9.2.9
|
||||
[9.2.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.7...9.2.8
|
||||
[9.2.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.6...9.2.7
|
||||
[9.2.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.5...9.2.6
|
||||
[9.2.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.4...9.2.5
|
||||
[9.2.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.3...9.2.4
|
||||
[9.2.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.2...9.2.3
|
||||
[9.2.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.1...9.2.2
|
||||
[9.2.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.0...9.2.1
|
||||
[9.2.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.11...9.2.0
|
||||
[9.1.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.10...9.1.11
|
||||
[9.1.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.9...9.1.10
|
||||
[9.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.8...9.1.9
|
||||
[9.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.7...9.1.8
|
||||
[9.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.6...9.1.7
|
||||
[9.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.5...9.1.6
|
||||
[9.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.4...9.1.5
|
||||
[9.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.3...9.1.4
|
||||
[9.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.2...9.1.3
|
||||
[9.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.1...9.1.2
|
||||
[9.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.0...9.1.1
|
||||
[9.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.0.0...9.1.0
|
||||
[9.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0...9.0.0
|
||||
[8.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0.1...8.0.2
|
||||
[8.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0.0...8.0.1
|
||||
[8.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.10...8.0.0
|
||||
[7.0.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.14...7.0.15
|
||||
[7.0.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.13...7.0.14
|
||||
[7.0.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.12...7.0.13
|
||||
[7.0.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.11...7.0.12
|
||||
[7.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.10...7.0.11
|
||||
[7.0.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.9...7.0.10
|
||||
[7.0.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.8...7.0.9
|
||||
[7.0.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.7...7.0.8
|
||||
[7.0.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.6...7.0.7
|
||||
[7.0.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.5...7.0.6
|
||||
[7.0.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.4...7.0.5
|
||||
[7.0.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.3...7.0.4
|
||||
[7.0.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.2...7.0.3
|
||||
[7.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.1...7.0.2
|
||||
[7.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.0...7.0.1
|
||||
[7.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/6.1.4...7.0.0
|
@ -0,0 +1,30 @@
|
||||
# Security Policy
|
||||
|
||||
If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure.
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
||||
|
||||
Instead, please email `sebastian@phpunit.de`.
|
||||
|
||||
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
|
||||
|
||||
* The type of issue
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Web Context
|
||||
|
||||
The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit.
|
||||
|
||||
The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes.
|
||||
|
||||
If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context.
|
||||
|
||||
Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes.
|
||||
|
@ -1,93 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Driver;
|
||||
|
||||
use const PHP_SAPI;
|
||||
use const PHP_VERSION;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function get_included_files;
|
||||
use function phpdbg_end_oplog;
|
||||
use function phpdbg_get_executable;
|
||||
use function phpdbg_start_oplog;
|
||||
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class PhpdbgDriver extends Driver
|
||||
{
|
||||
/**
|
||||
* @throws PhpdbgNotAvailableException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (PHP_SAPI !== 'phpdbg') {
|
||||
throw new PhpdbgNotAvailableException;
|
||||
}
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
phpdbg_start_oplog();
|
||||
}
|
||||
|
||||
public function stop(): RawCodeCoverageData
|
||||
{
|
||||
static $fetchedLines = [];
|
||||
|
||||
$dbgData = phpdbg_end_oplog();
|
||||
|
||||
if ($fetchedLines === []) {
|
||||
$sourceLines = phpdbg_get_executable();
|
||||
} else {
|
||||
$newFiles = array_diff(get_included_files(), array_keys($fetchedLines));
|
||||
|
||||
$sourceLines = [];
|
||||
|
||||
if ($newFiles) {
|
||||
$sourceLines = phpdbg_get_executable(['files' => $newFiles]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($sourceLines as $file => $lines) {
|
||||
foreach ($lines as $lineNo => $numExecuted) {
|
||||
$sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED;
|
||||
}
|
||||
}
|
||||
|
||||
$fetchedLines = array_merge($fetchedLines, $sourceLines);
|
||||
|
||||
return RawCodeCoverageData::fromXdebugWithoutPathCoverage(
|
||||
$this->detectExecutedLines($fetchedLines, $dbgData)
|
||||
);
|
||||
}
|
||||
|
||||
public function nameAndVersion(): string
|
||||
{
|
||||
return 'PHPDBG ' . PHP_VERSION;
|
||||
}
|
||||
|
||||
private function detectExecutedLines(array $sourceLines, array $dbgData): array
|
||||
{
|
||||
foreach ($dbgData as $file => $coveredLines) {
|
||||
foreach ($coveredLines as $lineNo => $numExecuted) {
|
||||
// phpdbg also reports $lineNo=0 when e.g. exceptions get thrown.
|
||||
// make sure we only mark lines executed which are actually executable.
|
||||
if (isset($sourceLines[$file][$lineNo])) {
|
||||
$sourceLines[$file][$lineNo] = self::LINE_EXECUTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sourceLines;
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Driver;
|
||||
|
||||
use const XDEBUG_CC_BRANCH_CHECK;
|
||||
use const XDEBUG_CC_DEAD_CODE;
|
||||
use const XDEBUG_CC_UNUSED;
|
||||
use const XDEBUG_FILTER_CODE_COVERAGE;
|
||||
use const XDEBUG_PATH_INCLUDE;
|
||||
use const XDEBUG_PATH_WHITELIST;
|
||||
use function defined;
|
||||
use function extension_loaded;
|
||||
use function ini_get;
|
||||
use function phpversion;
|
||||
use function sprintf;
|
||||
use function version_compare;
|
||||
use function xdebug_get_code_coverage;
|
||||
use function xdebug_set_filter;
|
||||
use function xdebug_start_code_coverage;
|
||||
use function xdebug_stop_code_coverage;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Xdebug2Driver extends Driver
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $pathCoverageIsMixedCoverage;
|
||||
|
||||
/**
|
||||
* @throws WrongXdebugVersionException
|
||||
* @throws Xdebug2NotEnabledException
|
||||
* @throws XdebugNotAvailableException
|
||||
*/
|
||||
public function __construct(Filter $filter)
|
||||
{
|
||||
if (!extension_loaded('xdebug')) {
|
||||
throw new XdebugNotAvailableException;
|
||||
}
|
||||
|
||||
if (version_compare(phpversion('xdebug'), '3', '>=')) {
|
||||
throw new WrongXdebugVersionException(
|
||||
sprintf(
|
||||
'This driver requires Xdebug 2 but version %s is loaded',
|
||||
phpversion('xdebug')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!ini_get('xdebug.coverage_enable')) {
|
||||
throw new Xdebug2NotEnabledException;
|
||||
}
|
||||
|
||||
if (!$filter->isEmpty()) {
|
||||
if (defined('XDEBUG_PATH_WHITELIST')) {
|
||||
$listType = XDEBUG_PATH_WHITELIST;
|
||||
} else {
|
||||
$listType = XDEBUG_PATH_INCLUDE;
|
||||
}
|
||||
|
||||
xdebug_set_filter(
|
||||
XDEBUG_FILTER_CODE_COVERAGE,
|
||||
$listType,
|
||||
$filter->files()
|
||||
);
|
||||
}
|
||||
|
||||
$this->pathCoverageIsMixedCoverage = version_compare(phpversion('xdebug'), '2.9.6', '<');
|
||||
}
|
||||
|
||||
public function canCollectBranchAndPathCoverage(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canDetectDeadCode(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$flags = XDEBUG_CC_UNUSED;
|
||||
|
||||
if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
|
||||
$flags |= XDEBUG_CC_DEAD_CODE;
|
||||
}
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
$flags |= XDEBUG_CC_BRANCH_CHECK;
|
||||
}
|
||||
|
||||
xdebug_start_code_coverage($flags);
|
||||
}
|
||||
|
||||
public function stop(): RawCodeCoverageData
|
||||
{
|
||||
$data = xdebug_get_code_coverage();
|
||||
|
||||
xdebug_stop_code_coverage();
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
if ($this->pathCoverageIsMixedCoverage) {
|
||||
return RawCodeCoverageData::fromXdebugWithMixedCoverage($data);
|
||||
}
|
||||
|
||||
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
|
||||
}
|
||||
|
||||
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
|
||||
}
|
||||
|
||||
public function nameAndVersion(): string
|
||||
{
|
||||
return 'Xdebug ' . phpversion('xdebug');
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Driver;
|
||||
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class PhpdbgNotAvailableException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The PHPDBG SAPI is not available');
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue