From ce7f5f5ab9dfafb32be18419b3bde282bb1bfa54 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sat, 29 Jul 2023 13:35:00 +0200 Subject: [PATCH 01/31] PHPUnit 10 --- .gitignore | 1 + CHANGELOG.md | 4 + composer.json | 15 +++- phpunit.xml | 33 +++---- .../Helpers/SerializationHelperTest.php | 90 +++++++++---------- 5 files changed, 71 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index bd38f2c..46e7e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ composer.lock *.DS_Store /.php_cs.cache /.phpunit.result.cache +/.phpunit.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fcbf78..adbeeca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.0.0 : TBD + +- **Changed** Support for PHP 8.1+ only + ## 1.3.0 : 2023-07-19 Changed minimal PHP version to `^7.1` in `composer.json`. Library still should work with PHP5.5+, if you have troubles diff --git a/composer.json b/composer.json index 5bdf28b..6e6d162 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,14 @@ "version": "1.3.0", "description": "A simple PHP library for GPX import/export", "minimum-stability": "stable", + "keywords": [ + "gpx", + "geospacial", + "parser", + "geo" + ], + "homepage": "https://sibyx.github.io/phpGPX/", + "readme": "README.md", "license": "MIT", "authors": [ { @@ -13,15 +21,14 @@ } ], "require": { - "php": ">=7.1", + "php": ">=8.1", "lib-libxml": "*", "ext-simplexml": "*", "ext-dom": "*" }, "require-dev": { - "evert/phpdoc-md" : "~0.2.0", - "phpunit/phpunit": "^9", - "friendsofphp/php-cs-fixer": "^2.18" + "phpunit/phpunit": "^10.2.6", + "friendsofphp/php-cs-fixer": "^v3.22.0" }, "autoload": { "psr-4": { "phpGPX\\": "src/phpGPX/" } diff --git a/phpunit.xml b/phpunit.xml index aa62e76..2918e45 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,25 +1,12 @@ - - - - - - - - - - - tests/ - - - + + + + + + + + tests/ + + diff --git a/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php b/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php index b214fb3..798ac81 100644 --- a/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php +++ b/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php @@ -45,49 +45,49 @@ public function testFilterNotNull($expected, $actual) $this->assertEquals($expected, SerializationHelper::filterNotNull($actual)); } - public function dataProviderFilterNotNull() - { - return [ - 'numeric 1' => [ - [], - [null], - ], - 'numeric 2' => [ - [], - [null, [null]], - ], - 'numeric 3' => [ - [1 => 1], - [null, 1], - ], - 'numeric 4' => [ - [1 => 1, 3 => 2], - [null, 1, null, 2, null], - ], - 'numeric 5' => [ - [1 => 1, 3 => 2, 5 => [0 => 3, 2 => 4], 6 => 5], - [null, 1, null, 2, null, [3, null, 4], 5, null], - ], - 'associative 1' => [ - [], - ["foo" => null], - ], - 'associative 2' => [ - [], - ["foo" => null, ["bar" => null]], - ], - 'associative 3' => [ - ["bar" => 1], - ["foo" => null, "bar" => 1], - ], - 'associative 4' => [ - ["bar" => 1, "caw" => 2], - ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null], - ], - 'associative 5' => [ - ["bar" => 1, "caw" => 2, "ere" => ["foo" => 3, "baz" => 4], "moo" => 5], - ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null, "ere" => ["foo" => 3, "bar" => null, "baz" => 4], "moo" => 5, "boo" => null], - ], - ]; - } + public static function dataProviderFilterNotNull(): array +{ + return [ + 'numeric 1' => [ + [], + [null], + ], + 'numeric 2' => [ + [], + [null, [null]], + ], + 'numeric 3' => [ + [1 => 1], + [null, 1], + ], + 'numeric 4' => [ + [1 => 1, 3 => 2], + [null, 1, null, 2, null], + ], + 'numeric 5' => [ + [1 => 1, 3 => 2, 5 => [0 => 3, 2 => 4], 6 => 5], + [null, 1, null, 2, null, [3, null, 4], 5, null], + ], + 'associative 1' => [ + [], + ["foo" => null], + ], + 'associative 2' => [ + [], + ["foo" => null, ["bar" => null]], + ], + 'associative 3' => [ + ["bar" => 1], + ["foo" => null, "bar" => 1], + ], + 'associative 4' => [ + ["bar" => 1, "caw" => 2], + ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null], + ], + 'associative 5' => [ + ["bar" => 1, "caw" => 2, "ere" => ["foo" => 3, "baz" => 4], "moo" => 5], + ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null, "ere" => ["foo" => 3, "bar" => null, "baz" => 4], "moo" => 5, "boo" => null], + ], + ]; + } } From 3fb1f95ffb45db650bfac660bf7c2512c35e24d4 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sat, 29 Jul 2023 20:04:22 +0200 Subject: [PATCH 02/31] AbstractParserTest removed --- .github/workflows/coverage.yml | 24 ++++ .github/workflows/phpunit.yml | 2 +- .gitignore | 1 + phpunit.xml | 34 +++-- src/phpGPX/Models/Bounds.php | 1 - .../phpGPX/Tests}/Helpers/GeoHelperTest.php | 6 +- .../Helpers/SerializationHelperTest.php | 15 ++- {tests => src/phpGPX/Tests}/LoadFileTest.php | 6 +- .../phpGPX/Tests}/LoadRouteFileTest.php | 12 +- .../Tests/Parsers/Bounds/BoundsParserTest.php | 78 ++++++++++++ .../phpGPX/Tests/Parsers/Bounds/bounds.json | 0 .../phpGPX/Tests/Parsers/Bounds/bounds.xml | 0 .../Tests/Parsers/Copyright/copyright.json | 0 .../Tests/Parsers/Copyright/copyright.xml | 0 .../phpGPX/Tests/Parsers/Email/email.json | 0 .../phpGPX/Tests/Parsers/Email/email.xml | 0 .../Parsers/Extension/ExtensionParserTest.php | 98 ++++++++++++++ .../Tests/Parsers/Extension/extension.json | 0 .../Tests/Parsers/Extension/extension.xml | 0 .../Tests/Parsers/Link/LinkParserTest.php | 75 +++++++++++ .../phpGPX/Tests/Parsers/Link/link.json | 0 .../phpGPX/Tests/Parsers/Link/link.xml | 0 .../Tests/Parsers/Person/PersonParserTest.php | 120 ++++++++++++++++++ .../phpGPX/Tests/Parsers/Person/person.json | 0 .../phpGPX/Tests/Parsers/Person/person.xml | 0 .../phpGPX/Tests}/fixtures/gps-track.gpx | 0 .../phpGPX/Tests}/fixtures/route.gpx | 0 .../phpGPX/Tests}/fixtures/timezero.gpx | 0 .../phpGPX/Parsers/AbstractParserTest.php | 77 ----------- .../phpGPX/Parsers/BoundsParserTest.php | 58 --------- .../phpGPX/Parsers/ExtensionParserTest.php | 89 ------------- .../phpGPX/Parsers/LinkParserTest.php | 66 ---------- .../phpGPX/Parsers/PersonParserTest.php | 89 ------------- 33 files changed, 455 insertions(+), 396 deletions(-) create mode 100644 .github/workflows/coverage.yml rename {tests/UnitTests/phpGPX => src/phpGPX/Tests}/Helpers/GeoHelperTest.php (89%) rename {tests/UnitTests/phpGPX => src/phpGPX/Tests}/Helpers/SerializationHelperTest.php (89%) rename {tests => src/phpGPX/Tests}/LoadFileTest.php (98%) rename {tests => src/phpGPX/Tests}/LoadRouteFileTest.php (96%) create mode 100644 src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php rename tests/UnitTests/phpGPX/Parsers/BoundsParserTest.json => src/phpGPX/Tests/Parsers/Bounds/bounds.json (100%) rename tests/UnitTests/phpGPX/Parsers/BoundsParserTest.xml => src/phpGPX/Tests/Parsers/Bounds/bounds.xml (100%) rename tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.json => src/phpGPX/Tests/Parsers/Copyright/copyright.json (100%) rename tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.xml => src/phpGPX/Tests/Parsers/Copyright/copyright.xml (100%) rename tests/UnitTests/phpGPX/Parsers/EmailParserTest.json => src/phpGPX/Tests/Parsers/Email/email.json (100%) rename tests/UnitTests/phpGPX/Parsers/EmailParserTest.xml => src/phpGPX/Tests/Parsers/Email/email.xml (100%) create mode 100644 src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php rename tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.json => src/phpGPX/Tests/Parsers/Extension/extension.json (100%) rename tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.xml => src/phpGPX/Tests/Parsers/Extension/extension.xml (100%) create mode 100644 src/phpGPX/Tests/Parsers/Link/LinkParserTest.php rename tests/UnitTests/phpGPX/Parsers/LinkParserTest.json => src/phpGPX/Tests/Parsers/Link/link.json (100%) rename tests/UnitTests/phpGPX/Parsers/LinkParserTest.xml => src/phpGPX/Tests/Parsers/Link/link.xml (100%) create mode 100644 src/phpGPX/Tests/Parsers/Person/PersonParserTest.php rename tests/UnitTests/phpGPX/Parsers/PersonParserTest.json => src/phpGPX/Tests/Parsers/Person/person.json (100%) rename tests/UnitTests/phpGPX/Parsers/PersonParserTest.xml => src/phpGPX/Tests/Parsers/Person/person.xml (100%) rename {tests => src/phpGPX/Tests}/fixtures/gps-track.gpx (100%) rename {tests => src/phpGPX/Tests}/fixtures/route.gpx (100%) rename {tests => src/phpGPX/Tests}/fixtures/timezero.gpx (100%) delete mode 100644 tests/UnitTests/phpGPX/Parsers/AbstractParserTest.php delete mode 100644 tests/UnitTests/phpGPX/Parsers/BoundsParserTest.php delete mode 100644 tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php delete mode 100644 tests/UnitTests/phpGPX/Parsers/LinkParserTest.php delete mode 100644 tests/UnitTests/phpGPX/Parsers/PersonParserTest.php diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..bbf39d3 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,24 @@ +name: Coverage + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + name: Install dependencies + - name: PHPUnit Tests + uses: php-actions/phpunit@v3 + env: + XDEBUG_MODE: coverage + with: + bootstrap: vendor/autoload.php + configuration: phpunit.xml + php_extensions: xdebug + args: tests --coverage-clover ./coverage.xml + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 72f54b6..ba4f9a0 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - php-version: ['7.3', '8.0', '8.1'] + php-version: ['8.1', '8.2'] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 46e7e3e..40a1a33 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ composer.lock /.php_cs.cache /.phpunit.result.cache /.phpunit.cache +coverage.xml \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 2918e45..8065e11 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,12 +1,26 @@ - - - - - - - - tests/ - - + + + + src/phpGPX/Tests + + + + + + src + + + src/phpGPX/Tests + + diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php index e2d41c3..38d5945 100644 --- a/src/phpGPX/Models/Bounds.php +++ b/src/phpGPX/Models/Bounds.php @@ -44,7 +44,6 @@ public function __construct() $this->maxLatitude = null; } - /** * Serialize object to array * @return array diff --git a/tests/UnitTests/phpGPX/Helpers/GeoHelperTest.php b/src/phpGPX/Tests/Helpers/GeoHelperTest.php similarity index 89% rename from tests/UnitTests/phpGPX/Helpers/GeoHelperTest.php rename to src/phpGPX/Tests/Helpers/GeoHelperTest.php index 84e8269..3137679 100644 --- a/tests/UnitTests/phpGPX/Helpers/GeoHelperTest.php +++ b/src/phpGPX/Tests/Helpers/GeoHelperTest.php @@ -3,7 +3,7 @@ * @author Jakub Dubec */ -namespace UnitTests\phpGPX\Helpers; +namespace phpGPX\Tests\Helpers; use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; @@ -18,6 +18,8 @@ class GeoHelperTest extends TestCase * Input points: * - 48.1573923225717 17.0547121910204 * - 48.1644916381763 17.0591753907502 + * @covers \phpGPX\Helpers\GeoHelper + * @covers \phpGPX\Models\Point */ public function testGetDistance() { @@ -38,6 +40,8 @@ public function testGetDistance() } /** + * @covers \phpGPX\Helpers\GeoHelper + * @covers \phpGPX\Models\Point * @link http://cosinekitty.com/compass.html */ public function testRealDistance() diff --git a/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php b/src/phpGPX/Tests/Helpers/SerializationHelperTest.php similarity index 89% rename from tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php rename to src/phpGPX/Tests/Helpers/SerializationHelperTest.php index 798ac81..b4acdda 100644 --- a/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php +++ b/src/phpGPX/Tests/Helpers/SerializationHelperTest.php @@ -3,13 +3,17 @@ * @author Jakub Dubec */ -namespace UnitTests\phpGPX\Helpers; +namespace phpGPX\Tests\Helpers; use phpGPX\Helpers\SerializationHelper; use PHPUnit\Framework\TestCase; class SerializationHelperTest extends TestCase { + /** + * @covers \phpGPX\Helpers\SerializationHelper + * @return void + */ public function testIntegerOrNull() { $this->assertNull(SerializationHelper::integerOrNull("")); @@ -19,6 +23,10 @@ public function testIntegerOrNull() $this->assertIsInt(SerializationHelper::integerOrNull("5")); } + /** + * @covers \phpGPX\Helpers\SerializationHelper + * @return void + */ public function testFloatOrNull() { $this->assertNull(SerializationHelper::floatOrNull("")); @@ -30,6 +38,10 @@ public function testFloatOrNull() $this->assertIsFloat(SerializationHelper::floatOrNull("5")); } + /** + * @covers \phpGPX\Helpers\SerializationHelper + * @return void + */ public function testStringOrNull() { $this->assertNull(SerializationHelper::stringOrNull(null)); @@ -38,6 +50,7 @@ public function testStringOrNull() } /** + * @covers \phpGPX\Helpers\SerializationHelper * @dataProvider dataProviderFilterNotNull */ public function testFilterNotNull($expected, $actual) diff --git a/tests/LoadFileTest.php b/src/phpGPX/Tests/LoadFileTest.php similarity index 98% rename from tests/LoadFileTest.php rename to src/phpGPX/Tests/LoadFileTest.php index c419664..a8a3b1f 100644 --- a/tests/LoadFileTest.php +++ b/src/phpGPX/Tests/LoadFileTest.php @@ -7,7 +7,11 @@ class LoadFileTest extends TestCase { - public function testLoadXmlFileGeneratedByTimezero() + /** + * @covers \phpGPX + * @return void + */ + public function testLoadXmlFileGeneratedByTimezero() { $file = __DIR__ . '/fixtures/timezero.gpx'; diff --git a/tests/LoadRouteFileTest.php b/src/phpGPX/Tests/LoadRouteFileTest.php similarity index 96% rename from tests/LoadRouteFileTest.php rename to src/phpGPX/Tests/LoadRouteFileTest.php index 71b370a..255a1b1 100644 --- a/tests/LoadRouteFileTest.php +++ b/src/phpGPX/Tests/LoadRouteFileTest.php @@ -11,7 +11,11 @@ class LoadRouteFileTest extends TestCase { - public function testRouteFile() + /** + * @covers \phpGPX + * @return void + */ + public function testRouteFile() { $file = __DIR__ . '/fixtures/route.gpx'; @@ -24,7 +28,11 @@ public function testRouteFile() $gpxFile->toXML()->saveXML(); } - public function testRouteFileWithSmoothedStats() + /** + * @covers \phpGPX + * @return void + */ + public function testRouteFileWithSmoothedStats() { $file = __DIR__ . '/fixtures/gps-track.gpx'; diff --git a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php new file mode 100644 index 0000000..7ee94fc --- /dev/null +++ b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php @@ -0,0 +1,78 @@ + + */ + +namespace phpGPX\Tests\Parsers\Bounds; + +use phpGPX\Models\Bounds; +use phpGPX\Parsers\BoundsParser; +use PHPUnit\Framework\TestCase; + +class BoundsParserTest extends TestCase +{ + protected Bounds $bounds; + protected \SimpleXMLElement $file; + + protected function setUp(): void + { + // Example object + $this->bounds = new Bounds(); + $this->bounds->maxLatitude = 49.090543; + $this->bounds->maxLongitude = 18.886939; + $this->bounds->minLatitude = 49.072489; + $this->bounds->minLongitude = 18.814543; + + // Input file + $this->file = simplexml_load_file(sprintf("%s/bounds.xml", __DIR__)); + } + + /** + * @covers \phpGPX + * @codeCoverageIgnore + * @return void + */ + public function testParse() + { + $bounds = BoundsParser::parse($this->file->bounds); + + $this->assertEquals($bounds, $this->bounds); + $this->assertNotEmpty($bounds); + + $this->assertEquals($this->bounds->maxLatitude, $bounds->maxLatitude); + $this->assertEquals($this->bounds->maxLongitude, $bounds->maxLongitude); + $this->assertEquals($this->bounds->minLatitude, $bounds->minLatitude); + $this->assertEquals($this->bounds->minLongitude, $bounds->minLongitude); + + $this->assertEquals($this->bounds->toArray(), $bounds->toArray()); + } + + /** + * @covers \phpGPX\Parsers\BoundsParser + * @covers \phpGPX\Models\Bounds + * @return void + * @throws \DOMException + */ + public function testToXML() + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(BoundsParser::toXML($this->bounds, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + /** + * @covers \phpGPX\Models\Bounds + * @return void + */ + public function testToJSON() + { + $this->assertJsonStringEqualsJsonFile( + sprintf("%s/bounds.json", __DIR__), json_encode($this->bounds->toArray()) + ); + } +} diff --git a/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.json b/src/phpGPX/Tests/Parsers/Bounds/bounds.json similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/BoundsParserTest.json rename to src/phpGPX/Tests/Parsers/Bounds/bounds.json diff --git a/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.xml b/src/phpGPX/Tests/Parsers/Bounds/bounds.xml similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/BoundsParserTest.xml rename to src/phpGPX/Tests/Parsers/Bounds/bounds.xml diff --git a/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.json b/src/phpGPX/Tests/Parsers/Copyright/copyright.json similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.json rename to src/phpGPX/Tests/Parsers/Copyright/copyright.json diff --git a/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.xml b/src/phpGPX/Tests/Parsers/Copyright/copyright.xml similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.xml rename to src/phpGPX/Tests/Parsers/Copyright/copyright.xml diff --git a/tests/UnitTests/phpGPX/Parsers/EmailParserTest.json b/src/phpGPX/Tests/Parsers/Email/email.json similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/EmailParserTest.json rename to src/phpGPX/Tests/Parsers/Email/email.json diff --git a/tests/UnitTests/phpGPX/Parsers/EmailParserTest.xml b/src/phpGPX/Tests/Parsers/Email/email.xml similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/EmailParserTest.xml rename to src/phpGPX/Tests/Parsers/Email/email.xml diff --git a/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php b/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php new file mode 100644 index 0000000..8ad7d16 --- /dev/null +++ b/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php @@ -0,0 +1,98 @@ +aTemp = (float) 14; + $trackpoint->avgTemperature = (float) 14; + $trackpoint->hr = (float) 152; + $trackpoint->heartRate = (float) 152; + + $this->extensions = new Extensions(); + $this->extensions->trackPointExtension = $trackpoint; + + $this->file = simplexml_load_file(sprintf("%s/extension.xml", __DIR__)); + } + + /** + * @covers \phpGPX\Parsers\ExtensionParser + * @covers \phpGPX\Parsers\Extensions\TrackPointExtensionParser + * @covers \phpGPX\Models\Extensions + * @covers \phpGPX\Helpers\SerializationHelper + * @covers \phpGPX\Models\Extensions\AbstractExtension + * @covers \phpGPX\Models\Extensions\TrackPointExtension + * @return void + */ + public function testParse() + { + $extensions = ExtensionParser::parse($this->file->extensions); + + $this->assertEquals($this->extensions, $extensions); + + $this->assertEquals($this->extensions->unsupported, $extensions->unsupported); + $this->assertEquals($this->extensions->trackPointExtension, $extensions->trackPointExtension); + + $this->assertEquals($this->extensions->toArray(), $extensions->toArray()); + } + + /** + * @covers \phpGPX\Parsers\ExtensionParser + * @covers \phpGPX\Parsers\Extensions\TrackPointExtensionParser + * @covers \phpGPX\Models\Extensions + * @covers \phpGPX\Models\Extensions\AbstractExtension + * @covers \phpGPX\Models\Extensions\TrackPointExtension + * @return void + * @throws \DOMException + */ + public function testToXML() + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(ExtensionParser::toXML($this->extensions, $document)); + + $attributes = [ + 'xmlns' => 'http://www.topografix.com/GPX/1/1', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation' => 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd', + 'xmlns:gpxtpx' => 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1', + 'xmlns:gpxx' => 'http://www.garmin.com/xmlschemas/GpxExtensions/v3', + ]; + + foreach ($attributes as $key => $value) { + $attribute = $document->createAttribute($key); + $attribute->value = $value; + $root->appendChild($attribute); + } + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + /** + * @covers \phpGPX\Models\Extensions + * @covers \phpGPX\Models\Extensions\AbstractExtension + * @covers \phpGPX\Models\Extensions\TrackPointExtension + * @covers \phpGPX\Helpers\SerializationHelper + * @return void + */ + public function testToJSON() + { + $this->assertJsonStringEqualsJsonFile( + sprintf("%s/extension.json", __DIR__), json_encode($this->extensions->toArray()) + ); + } +} diff --git a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.json b/src/phpGPX/Tests/Parsers/Extension/extension.json similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.json rename to src/phpGPX/Tests/Parsers/Extension/extension.json diff --git a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.xml b/src/phpGPX/Tests/Parsers/Extension/extension.xml similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.xml rename to src/phpGPX/Tests/Parsers/Extension/extension.xml diff --git a/src/phpGPX/Tests/Parsers/Link/LinkParserTest.php b/src/phpGPX/Tests/Parsers/Link/LinkParserTest.php new file mode 100644 index 0000000..7fa728c --- /dev/null +++ b/src/phpGPX/Tests/Parsers/Link/LinkParserTest.php @@ -0,0 +1,75 @@ + + */ + +namespace phpGPX\Tests\Parsers\Link; + +use phpGPX\Models\Link; +use phpGPX\Parsers\LinkParser; +use PHPUnit\Framework\TestCase; + +class LinkParserTest extends TestCase +{ + protected Link $link; + protected \SimpleXMLElement $file; + + protected function setUp(): void + { + $this->link = new Link(); + $this->link->href = "https://jakubdubec.me"; + $this->link->text = "Portfolio"; + $this->link->type = "text/html"; + + $this->file = simplexml_load_file(sprintf("%s/link.xml", __DIR__)); + } + + /** + * @covers \phpGPX\Parsers\LinkParser + * @covers \phpGPX\Models\Link + * @return void + */ + public function testParse() + { + $links = LinkParser::parse($this->file->link); + + $this->assertNotEmpty($links); + $this->assertEquals($this->link, $links[0]); + + $this->assertEquals($this->link->href, $links[0]->href); + $this->assertEquals($this->link->text, $links[0]->text); + $this->assertEquals($this->link->type, $links[0]->type); + + $this->assertEquals($this->link->toArray(), $links[0]->toArray()); + } + + + /** + * @covers \phpGPX\Parsers\LinkParser + * @covers \phpGPX\Models\Link + * @return void + * @throws \DOMException + */ + public function testToXML() + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(LinkParser::toXML($this->link, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + /** + * @covers \phpGPX\Models\Link + * @return void + */ + public function testToJSON() + { + $this->assertJsonStringEqualsJsonFile( + sprintf("%s/link.json", __DIR__), json_encode($this->link->toArray()) + ); + } +} diff --git a/tests/UnitTests/phpGPX/Parsers/LinkParserTest.json b/src/phpGPX/Tests/Parsers/Link/link.json similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/LinkParserTest.json rename to src/phpGPX/Tests/Parsers/Link/link.json diff --git a/tests/UnitTests/phpGPX/Parsers/LinkParserTest.xml b/src/phpGPX/Tests/Parsers/Link/link.xml similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/LinkParserTest.xml rename to src/phpGPX/Tests/Parsers/Link/link.xml diff --git a/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php b/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php new file mode 100644 index 0000000..032cc74 --- /dev/null +++ b/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php @@ -0,0 +1,120 @@ + + */ + +namespace phpGPX\Tests\Parsers\Person; + +use phpGPX\Models\Email; +use phpGPX\Models\GpxFile; +use phpGPX\Models\Link; +use phpGPX\Models\Metadata; +use phpGPX\Models\Person; +use phpGPX\Parsers\PersonParser; +use PHPUnit\Framework\TestCase; + +class PersonParserTest extends TestCase +{ + protected Person $person; + protected \SimpleXMLElement $file; + + protected function setUp(): void + { + $this->person = new Person(); + $this->person->name = "Jakub Dubec"; + + $email = new Email(); + $email->id = "jakub.dubec"; + $email->domain = "gmail.com"; + $this->person->email = $email; + + $link = new Link(); + $link->href = "https://jakubdubec.me"; + $link->text = "Portfolio"; + $link->type = "text/html"; + $this->person->links[] = $link; + + $this->file = simplexml_load_file(sprintf("%s/person.xml", __DIR__)); + } + + /** + * @covers \phpGPX + * @return void + */ + public function testParse() + { + $person = PersonParser::parse($this->file->author); + + $this->assertNotEmpty($person); + + // Primitive attributes + $this->assertEquals($this->person->name, $person->name); + $this->assertEquals($this->person, $person); + + // Email + $this->assertEquals($this->person->email->id, $person->email->id); + $this->assertEquals($this->person->email->domain, $person->email->domain); + + // Link + $this->assertEquals($this->person->links[0]->type, $person->links[0]->type); + $this->assertEquals($this->person->links[0]->text, $person->links[0]->text); + $this->assertEquals($this->person->links[0]->href, $person->links[0]->href); + + // toArray functions + $this->assertEquals($this->person->toArray(), $person->toArray()); + $this->assertEquals($this->person->email->toArray(), $person->email->toArray()); + $this->assertEquals($this->person->links[0]->toArray(), $person->links[0]->toArray()); + } + + /** + * @covers \phpGPX + * @url https://github.com/Sibyx/phpGPX/issues/48 + */ + public function testEmptyLinks() + { + $gpx_file = new GpxFile(); + + $gpx_file->metadata = new Metadata(); + $gpx_file->metadata->author = new Person(); + $gpx_file->metadata->author->name = "Arthur Dent"; + + $this->assertNotNull($gpx_file->toXML()->saveXML()); + } + + + /** + * @covers \phpGPX\Models\Email + * @covers \phpGPX\Models\Link + * @covers \phpGPX\Models\Person + * @covers \phpGPX\Parsers\EmailParser + * @covers \phpGPX\Parsers\LinkParser + * @covers \phpGPX\Parsers\PersonParser + * @return void + * @throws \DOMException + */ + public function testToXML() + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(PersonParser::toXML($this->person, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + /** + * @covers \phpGPX\Models\Person + * @covers \phpGPX\Models\Email + * @covers \phpGPX\Models\Link + * @covers \phpGPX\Helpers\SerializationHelper + * @return void + */ + public function testToJSON() + { + $this->assertJsonStringEqualsJsonFile( + sprintf("%s/person.json", __DIR__), json_encode($this->person->toArray()) + ); + } +} diff --git a/tests/UnitTests/phpGPX/Parsers/PersonParserTest.json b/src/phpGPX/Tests/Parsers/Person/person.json similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/PersonParserTest.json rename to src/phpGPX/Tests/Parsers/Person/person.json diff --git a/tests/UnitTests/phpGPX/Parsers/PersonParserTest.xml b/src/phpGPX/Tests/Parsers/Person/person.xml similarity index 100% rename from tests/UnitTests/phpGPX/Parsers/PersonParserTest.xml rename to src/phpGPX/Tests/Parsers/Person/person.xml diff --git a/tests/fixtures/gps-track.gpx b/src/phpGPX/Tests/fixtures/gps-track.gpx similarity index 100% rename from tests/fixtures/gps-track.gpx rename to src/phpGPX/Tests/fixtures/gps-track.gpx diff --git a/tests/fixtures/route.gpx b/src/phpGPX/Tests/fixtures/route.gpx similarity index 100% rename from tests/fixtures/route.gpx rename to src/phpGPX/Tests/fixtures/route.gpx diff --git a/tests/fixtures/timezero.gpx b/src/phpGPX/Tests/fixtures/timezero.gpx similarity index 100% rename from tests/fixtures/timezero.gpx rename to src/phpGPX/Tests/fixtures/timezero.gpx diff --git a/tests/UnitTests/phpGPX/Parsers/AbstractParserTest.php b/tests/UnitTests/phpGPX/Parsers/AbstractParserTest.php deleted file mode 100644 index 4272104..0000000 --- a/tests/UnitTests/phpGPX/Parsers/AbstractParserTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ - -namespace UnitTests\phpGPX\Parsers; - -use phpGPX\Models\Summarizable; -use PHPUnit\Framework\TestCase; - -abstract class AbstractParserTest extends TestCase -{ - /** - * @var \SimpleXMLElement - */ - protected $testXmlFile; - - /** - * Instance of model holding data for parser. - * EXAMPLE: model phpGPX\Models\Bounds belongs to parser phpGPX\Parsers\BoundsParser - * @var Summarizable - */ - protected $testModelInstance; - - /** - * Full name with namespace for models class. - * EXAMPLE: phpGPX\Models\Bounds - * @var string - */ - protected $testModelClass; - - /** - * Full name with namespace for parser class. - * EXAMPLE: phpGPX\Parsers\BoundsParser - * @var string - */ - protected $testParserClass; - - protected function setUp(): void - { - $reflection = new \ReflectionClass($this->testParserClass); - - $this->testXmlFile = simplexml_load_file(sprintf("%s/%sTest.xml", __DIR__, $reflection->getShortName())); - } - - abstract public function testParse(); - - /** - * Returns output of ::toXML method of tested parser. - * @depends testParse - * @param \DOMDocument $document - * @return \DOMElement - */ - abstract protected function convertToXML(\DOMDocument $document); - - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild($this->convertToXML($document)); - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->testXmlFile->asXML(), $document->saveXML()); - } - - public function testToJSON() - { - $reflection = new \ReflectionClass($this->testParserClass); - - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/%sTest.json", __DIR__, $reflection->getShortName()), - json_encode($this->testModelInstance->toArray()) - ); - } -} diff --git a/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.php b/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.php deleted file mode 100644 index 05f5654..0000000 --- a/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ - -namespace UnitTests\phpGPX\Parsers; - -use phpGPX\Models\Bounds; -use phpGPX\Parsers\BoundsParser; - -class BoundsParserTest extends AbstractParserTest -{ - protected $testModelClass = Bounds::class; - protected $testParserClass = BoundsParser::class; - - /** - * @var Bounds - */ - protected $testModelInstance; - - public static function createTestInstance() - { - $bounds = new Bounds(); - - $bounds->maxLatitude = 49.090543; - $bounds->maxLongitude = 18.886939; - $bounds->minLatitude = 49.072489; - $bounds->minLongitude = 18.814543; - - return $bounds; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->testModelInstance = self::createTestInstance(); - } - - public function testParse() - { - $bounds = BoundsParser::parse($this->testXmlFile->bounds); - - $this->assertNotEmpty($bounds); - - $this->assertEquals($this->testModelInstance->maxLatitude, $bounds->maxLatitude); - $this->assertEquals($this->testModelInstance->maxLongitude, $bounds->maxLongitude); - $this->assertEquals($this->testModelInstance->minLatitude, $bounds->minLatitude); - $this->assertEquals($this->testModelInstance->minLongitude, $bounds->minLongitude); - - $this->assertEquals($this->testModelInstance->toArray(), $bounds->toArray()); - } - - protected function convertToXML(\DOMDocument $document) - { - return BoundsParser::toXML($this->testModelInstance, $document); - } -} diff --git a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php b/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php deleted file mode 100644 index 54f67dc..0000000 --- a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php +++ /dev/null @@ -1,89 +0,0 @@ -aTemp = (float) 14; - $trackpoint->avgTemperature = (float) 14; - $trackpoint->hr = (float) 152; - $trackpoint->heartRate = (float) 152; - - $extensions = new Extensions(); - $extensions->trackPointExtension = $trackpoint; - - return $extensions; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->testModelInstance = self::createTestInstance(); - } - - public function testParse() - { - $extensions = ExtensionParser::parse($this->testXmlFile->extensions); - - $this->assertEquals($this->testModelInstance->unsupported, $extensions->unsupported); - $this->assertEquals($this->testModelInstance->trackPointExtension, $extensions->trackPointExtension); - - $this->assertEquals($this->testModelInstance->toArray(), $extensions->toArray()); - } - - - /** - * Returns output of ::toXML method of tested parser. - * @param \DOMDocument $document - * @return \DOMElement - */ - protected function convertToXML(\DOMDocument $document) - { - return ExtensionParser::toXML($this->testModelInstance, $document); - } - - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild($this->convertToXML($document)); - - $attributes = [ - 'xmlns' => 'http://www.topografix.com/GPX/1/1', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:schemaLocation' => 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd', - 'xmlns:gpxtpx' => 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1', - 'xmlns:gpxx' => 'http://www.garmin.com/xmlschemas/GpxExtensions/v3', - ]; - - foreach ($attributes as $key => $value) { - $attribute = $document->createAttribute($key); - $attribute->value = $value; - $root->appendChild($attribute); - } - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->testXmlFile->asXML(), $document->saveXML()); - } -} diff --git a/tests/UnitTests/phpGPX/Parsers/LinkParserTest.php b/tests/UnitTests/phpGPX/Parsers/LinkParserTest.php deleted file mode 100644 index 1ec868b..0000000 --- a/tests/UnitTests/phpGPX/Parsers/LinkParserTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ - -namespace UnitTests\phpGPX\Parsers; - -use phpGPX\Models\Link; -use phpGPX\Parsers\LinkParser; - -class LinkParserTest extends AbstractParserTest -{ - protected $testModelClass = Link::class; - protected $testParserClass = LinkParser::class; - - /** - * @var Link - */ - protected $testModelInstance; - - /** - * @return Link - */ - public static function createTestInstance() - { - $link = new Link(); - $link->href = "https://jakubdubec.me"; - $link->text = "Portfolio"; - $link->type = "text/html"; - - return $link; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->testModelInstance = self::createTestInstance(); - } - - public function testParse() - { - $links = LinkParser::parse($this->testXmlFile->link); - - $this->assertNotEmpty($links); - - $link = $links[0]; - - $this->assertEquals($this->testModelInstance->href, $link->href); - $this->assertEquals($this->testModelInstance->text, $link->text); - $this->assertEquals($this->testModelInstance->type, $link->type); - - $this->assertEquals($this->testModelInstance->toArray(), $link->toArray()); - } - - - /** - * Returns output of ::toXML method of tested parser. - * @param \DOMDocument $document - * @return \DOMElement - */ - protected function convertToXML(\DOMDocument $document) - { - return LinkParser::toXML($this->testModelInstance, $document); - } -} diff --git a/tests/UnitTests/phpGPX/Parsers/PersonParserTest.php b/tests/UnitTests/phpGPX/Parsers/PersonParserTest.php deleted file mode 100644 index 85b6761..0000000 --- a/tests/UnitTests/phpGPX/Parsers/PersonParserTest.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ - -namespace UnitTests\phpGPX\Parsers; - -use phpGPX\Models\GpxFile; -use phpGPX\Models\Metadata; -use phpGPX\Models\Person; -use phpGPX\Parsers\PersonParser; - -class PersonParserTest extends AbstractParserTest -{ - protected $testModelClass = Person::class; - protected $testParserClass = PersonParser::class; - - /** - * @var Person - */ - protected $testModelInstance; - - public static function createTestInstance() - { - $person = new Person(); - $person->name = "Jakub Dubec"; - $person->email = EmailParserTest::createTestInstance(); - $person->links[] = LinkParserTest::createTestInstance(); - $person->name = 'Jakub Dubec'; - - return $person; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->testModelInstance = self::createTestInstance(); - } - - public function testParse() - { - $person = PersonParser::parse($this->testXmlFile->author); - - $this->assertNotEmpty($person); - - // Primitive attributes - $this->assertEquals($this->testModelInstance->name, $person->name); - - // Email - $this->assertEquals($this->testModelInstance->email->id, $person->email->id); - $this->assertEquals($this->testModelInstance->email->domain, $person->email->domain); - - // Link - $this->assertEquals($this->testModelInstance->links[0]->type, $person->links[0]->type); - $this->assertEquals($this->testModelInstance->links[0]->text, $person->links[0]->text); - $this->assertEquals($this->testModelInstance->links[0]->href, $person->links[0]->href); - - // toArray functions - $this->assertEquals($this->testModelInstance->toArray(), $person->toArray()); - $this->assertEquals($this->testModelInstance->email->toArray(), $person->email->toArray()); - $this->assertEquals($this->testModelInstance->links[0]->toArray(), $person->links[0]->toArray()); - } - - /** - * Returns output of ::toXML method of tested parser. - * @depends testParse - * @param \DOMDocument $document - * @return \DOMElement - */ - protected function convertToXML(\DOMDocument $document) - { - return PersonParser::toXML($this->testModelInstance, $document); - } - - /** - * @url https://github.com/Sibyx/phpGPX/issues/48 - */ - public function testEmptyLinks() - { - $gpx_file = new GpxFile(); - - $gpx_file->metadata = new Metadata(); - $gpx_file->metadata->author = new Person(); - $gpx_file->metadata->author->name = "Arthur Dent"; - - $this->assertNotNull($gpx_file->toXML()->saveXML()); - } -} From 0b65bd366b6b7bac7228cc2b60de2d1bb415c426 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sat, 29 Jul 2023 20:06:04 +0200 Subject: [PATCH 03/31] AbstractParserTest removed --- .../phpGPX/Tests}/CreateWaypointTest.php | 12 ++- .../Tests}/Helpers/DateTimeHelperTest.php | 24 +++++- .../Parsers/Copyright/CopyrightParserTest.php | 77 +++++++++++++++++++ .../Tests/Parsers/Email/EmailParserTest.php | 73 ++++++++++++++++++ .../phpGPX/Parsers/CopyrightParserTest.php | 62 --------------- .../phpGPX/Parsers/EmailParserTest.php | 61 --------------- 6 files changed, 179 insertions(+), 130 deletions(-) rename {tests => src/phpGPX/Tests}/CreateWaypointTest.php (97%) rename {tests/UnitTests/phpGPX => src/phpGPX/Tests}/Helpers/DateTimeHelperTest.php (75%) create mode 100644 src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php create mode 100644 src/phpGPX/Tests/Parsers/Email/EmailParserTest.php delete mode 100644 tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.php delete mode 100644 tests/UnitTests/phpGPX/Parsers/EmailParserTest.php diff --git a/tests/CreateWaypointTest.php b/src/phpGPX/Tests/CreateWaypointTest.php similarity index 97% rename from tests/CreateWaypointTest.php rename to src/phpGPX/Tests/CreateWaypointTest.php index 6b2ab9e..fdf1f4b 100644 --- a/tests/CreateWaypointTest.php +++ b/src/phpGPX/Tests/CreateWaypointTest.php @@ -1,14 +1,13 @@ waypoint_created_file}"); system("rm -f {$this->waypoint_saved_file}"); } + + /** + * @covers \phpGPX + * @return void + */ public function test_waypoints_load() { $origFile = $this->waypoint_created_file; diff --git a/tests/UnitTests/phpGPX/Helpers/DateTimeHelperTest.php b/src/phpGPX/Tests/Helpers/DateTimeHelperTest.php similarity index 75% rename from tests/UnitTests/phpGPX/Helpers/DateTimeHelperTest.php rename to src/phpGPX/Tests/Helpers/DateTimeHelperTest.php index 2374ce5..ca27a65 100644 --- a/tests/UnitTests/phpGPX/Helpers/DateTimeHelperTest.php +++ b/src/phpGPX/Tests/Helpers/DateTimeHelperTest.php @@ -3,7 +3,7 @@ * @author Jakub Dubec */ -namespace UnitTests\phpGPX\Helpers; +namespace phpGPX\Tests\Helpers; use phpGPX\Helpers\DateTimeHelper; use phpGPX\Models\Point; @@ -11,6 +11,12 @@ class DateTimeHelperTest extends TestCase { + /** + * @covers \phpGPX\Helpers\DateTimeHelper + * @covers \phpGPX\Models\Point + * @return void + * @throws \Exception + */ public function testComparePointsByTimestamp() { // 2017-08-12T20:16:29+00:00 @@ -26,7 +32,11 @@ public function testComparePointsByTimestamp() $this->assertTrue(($time1 > $time2) && DateTimeHelper::comparePointsByTimestamp($point1, $point2)); } - public function testFormatDateTime() + /** + * @covers \phpGPX\Helpers\DateTimeHelper::formatDateTime + * @return void + */ + public function testFormatDateTime() { // 1. Basic test $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); @@ -55,6 +65,10 @@ public function testFormatDateTime() ); } + /** + * @covers \phpGPX\Helpers\DateTimeHelper::parseDateTime + * @return void + */ public function testParseDateTime() { // 1. Valid string @@ -64,7 +78,11 @@ public function testParseDateTime() ); } - public function testParseDateTimeInvalidInput() + /** + * @covers \phpGPX\Helpers\DateTimeHelper::parseDateTime + * @return void + */ + public function testParseDateTimeInvalidInput() { $this->expectException("Exception"); DateTimeHelper::parseDateTime("Invalid exception"); diff --git a/src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php b/src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php new file mode 100644 index 0000000..bf393a3 --- /dev/null +++ b/src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php @@ -0,0 +1,77 @@ + + */ + +namespace phpGPX\Tests\Parsers\Copyright; + +use phpGPX\Models\Copyright; +use phpGPX\Parsers\CopyrightParser; +use PHPUnit\Framework\TestCase; + +class CopyrightParserTest extends TestCase +{ + protected Copyright $copyright; + protected \SimpleXMLElement $file; + + protected function setUp(): void + { + $this->copyright = new Copyright(); + $this->copyright->author = "Jakub Dubec"; + $this->copyright->license = "https://github.com/Sibyx/phpGPX/blob/master/LICENSE"; + $this->copyright->year = '2017'; + + // Input file + $this->file = simplexml_load_file(sprintf("%s/copyright.xml", __DIR__)); + } + + /** + * @covers \phpGPX\Parsers\CopyrightParser + * @covers \phpGPX\Helpers\SerializationHelper + * @covers \phpGPX\Models\Copyright + * @return void + */ + public function testParse() + { + $copyright = CopyrightParser::parse($this->file->copyright); + + $this->assertEquals($this->copyright, $copyright); + $this->assertNotEmpty($copyright); + + $this->assertEquals($this->copyright->author, $copyright->author); + $this->assertEquals($this->copyright->license, $copyright->license); + $this->assertEquals($this->copyright->year, $copyright->year); + + $this->assertEquals($this->copyright->toArray(), $copyright->toArray()); + } + + /** + * @covers \phpGPX\Parsers\CopyrightParser + * @covers \phpGPX\Models\Copyright + * @return void + * @throws \DOMException + */ + public function testToXML() + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(CopyrightParser::toXML($this->copyright, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + /** + * @covers \phpGPX\Models\Copyright + * @covers \phpGPX\Helpers\SerializationHelper + * @return void + */ + public function testToJSON() + { + $this->assertJsonStringEqualsJsonFile( + sprintf("%s/copyright.json", __DIR__), json_encode($this->copyright->toArray()) + ); + } +} diff --git a/src/phpGPX/Tests/Parsers/Email/EmailParserTest.php b/src/phpGPX/Tests/Parsers/Email/EmailParserTest.php new file mode 100644 index 0000000..c421dcc --- /dev/null +++ b/src/phpGPX/Tests/Parsers/Email/EmailParserTest.php @@ -0,0 +1,73 @@ + + */ + +namespace phpGPX\Tests\Parsers\Email; + +use phpGPX\Models\Email; +use phpGPX\Parsers\EmailParser; +use PHPUnit\Framework\TestCase; + +class EmailParserTest extends TestCase +{ + protected Email $email; + protected \SimpleXMLElement $file; + + protected function setUp(): void + { + $this->email = new Email(); + $this->email->id = "jakub.dubec"; + $this->email->domain = "gmail.com"; + + $this->file = simplexml_load_file(sprintf("%s/email.xml", __DIR__)); + } + + /** + * @covers \phpGPX\Parsers\EmailParser + * @covers \phpGPX\Models\Email + * @return void + */ + public function testParse() + { + $email = EmailParser::parse($this->file->email); + + $this->assertEquals($this->email, $email); + $this->assertNotEmpty($email); + + $this->assertEquals($this->email->id, $email->id); + $this->assertEquals($this->email->domain, $email->domain); + + $this->assertEquals($this->email->toArray(), $email->toArray()); + } + + + /** + * @covers \phpGPX\Parsers\EmailParser + * @covers \phpGPX\Models\Email + * @return void + * @throws \DOMException + */ + public function testToXML() + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(EmailParser::toXML($this->email, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + /** + * @covers \phpGPX\Models\Email + * @return void + */ + public function testToJSON() + { + $this->assertJsonStringEqualsJsonFile( + sprintf("%s/email.json", __DIR__), json_encode($this->email->toArray()) + ); + } +} diff --git a/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.php b/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.php deleted file mode 100644 index 491b745..0000000 --- a/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ - -namespace phpGPX\Tests\UnitTests\phpGPX\Parsers; - -use phpGPX\Models\Copyright; -use phpGPX\Parsers\CopyrightParser; -use UnitTests\phpGPX\Parsers\AbstractParserTest; - -class CopyrightParserTest extends AbstractParserTest -{ - protected $testModelClass = Copyright::class; - protected $testParserClass = CopyrightParser::class; - - /** - * @var Copyright - */ - protected $testModelInstance; - - public static function createTestInstance() - { - $copyright = new Copyright(); - - $copyright->author = "Jakub Dubec"; - $copyright->license = "https://github.com/Sibyx/phpGPX/blob/master/LICENSE"; - $copyright->year = '2017'; - - return $copyright; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->testModelInstance = self::createTestInstance(); - } - - public function testParse() - { - $copyright = CopyrightParser::parse($this->testXmlFile->copyright); - - $this->assertNotEmpty($copyright); - - $this->assertEquals($this->testModelInstance->author, $copyright->author); - $this->assertEquals($this->testModelInstance->license, $copyright->license); - $this->assertEquals($this->testModelInstance->year, $copyright->year); - - $this->assertEquals($this->testModelInstance->toArray(), $copyright->toArray()); - } - - /** - * Returns output of ::toXML method of tested parser. - * @param \DOMDocument $document - * @return \DOMElement - */ - protected function convertToXML(\DOMDocument $document) - { - return CopyrightParser::toXML($this->testModelInstance, $document); - } -} diff --git a/tests/UnitTests/phpGPX/Parsers/EmailParserTest.php b/tests/UnitTests/phpGPX/Parsers/EmailParserTest.php deleted file mode 100644 index 43b8bd7..0000000 --- a/tests/UnitTests/phpGPX/Parsers/EmailParserTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ - -namespace UnitTests\phpGPX\Parsers; - -use phpGPX\Models\Email; -use phpGPX\Parsers\EmailParser; - -class EmailParserTest extends AbstractParserTest -{ - protected $testModelClass = Email::class; - protected $testParserClass = EmailParser::class; - - /** - * @var Email - */ - protected $testModelInstance; - - public static function createTestInstance() - { - $email = new Email(); - - $email->id = "jakub.dubec"; - $email->domain = "gmail.com"; - - return $email; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->testModelInstance = self::createTestInstance(); - } - - - public function testParse() - { - $email = EmailParser::parse($this->testXmlFile->email); - - $this->assertNotEmpty($email); - - $this->assertEquals($this->testModelInstance->id, $email->id); - $this->assertEquals($this->testModelInstance->domain, $email->domain); - - $this->assertEquals($this->testModelInstance->toArray(), $email->toArray()); - } - - /** - * Returns output of ::toXML method of tested parser. - * @depends testParse - * @param \DOMDocument $document - * @return \DOMElement - */ - protected function convertToXML(\DOMDocument $document) - { - return EmailParser::toXML($this->testModelInstance, $document); - } -} From b354e1a56852141c71612d88f7da2420a582a6e9 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sat, 29 Jul 2023 20:08:21 +0200 Subject: [PATCH 04/31] Fixed coverage action --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bbf39d3..915acf6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,7 +17,7 @@ jobs: bootstrap: vendor/autoload.php configuration: phpunit.xml php_extensions: xdebug - args: tests --coverage-clover ./coverage.xml + args: --coverage-clover ./coverage.xml - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: From 569bf80dff027b627f39637891d0330701adbc4f Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sat, 29 Jul 2023 20:17:18 +0200 Subject: [PATCH 05/31] Fixed PHPUnit annotation warnings --- src/phpGPX/Tests/CreateWaypointTest.php | 2 +- src/phpGPX/Tests/LoadFileTest.php | 2 +- src/phpGPX/Tests/LoadRouteFileTest.php | 4 ++-- src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php | 4 ++-- src/phpGPX/Tests/Parsers/Person/PersonParserTest.php | 10 ++++++++-- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/phpGPX/Tests/CreateWaypointTest.php b/src/phpGPX/Tests/CreateWaypointTest.php index fdf1f4b..acec001 100644 --- a/src/phpGPX/Tests/CreateWaypointTest.php +++ b/src/phpGPX/Tests/CreateWaypointTest.php @@ -106,7 +106,7 @@ public function tearDown(): void } /** - * @covers \phpGPX + * @coversNothing * @return void */ public function test_waypoints_load() diff --git a/src/phpGPX/Tests/LoadFileTest.php b/src/phpGPX/Tests/LoadFileTest.php index a8a3b1f..37f5104 100644 --- a/src/phpGPX/Tests/LoadFileTest.php +++ b/src/phpGPX/Tests/LoadFileTest.php @@ -8,7 +8,7 @@ class LoadFileTest extends TestCase { /** - * @covers \phpGPX + * @coversNothing * @return void */ public function testLoadXmlFileGeneratedByTimezero() diff --git a/src/phpGPX/Tests/LoadRouteFileTest.php b/src/phpGPX/Tests/LoadRouteFileTest.php index 255a1b1..d474684 100644 --- a/src/phpGPX/Tests/LoadRouteFileTest.php +++ b/src/phpGPX/Tests/LoadRouteFileTest.php @@ -12,7 +12,7 @@ class LoadRouteFileTest extends TestCase { /** - * @covers \phpGPX + * @coversNothing * @return void */ public function testRouteFile() @@ -29,7 +29,7 @@ public function testRouteFile() } /** - * @covers \phpGPX + * @coversNothing * @return void */ public function testRouteFileWithSmoothedStats() diff --git a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php index 7ee94fc..b1f39db 100644 --- a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php +++ b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php @@ -28,8 +28,8 @@ protected function setUp(): void } /** - * @covers \phpGPX - * @codeCoverageIgnore + * @covers \phpGPX\Parsers\BoundsParser + * @covers \phpGPX\Models\Bounds * @return void */ public function testParse() diff --git a/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php b/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php index 032cc74..3e02d13 100644 --- a/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php +++ b/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php @@ -38,7 +38,13 @@ protected function setUp(): void } /** - * @covers \phpGPX + * @covers \phpGPX\Models\Person + * @covers \phpGPX\Models\Link + * @covers \phpGPX\Models\Email + * @covers \phpGPX\Parsers\EmailParser + * @covers \phpGPX\Parsers\LinkParser + * @covers \phpGPX\Parsers\PersonParser + * @covers \phpGPX\Helpers\SerializationHelper * @return void */ public function testParse() @@ -67,7 +73,7 @@ public function testParse() } /** - * @covers \phpGPX + * @coversNothing * @url https://github.com/Sibyx/phpGPX/issues/48 */ public function testEmptyLinks() From 5e8c7a72448688d8064adf092ff9e0926080f67d Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 30 Jul 2023 13:32:44 +0200 Subject: [PATCH 06/31] Introducing typing in extensions, bounds, routes and tracks --- src/phpGPX/Helpers/DateTimeHelper.php | 36 ++++---- src/phpGPX/Helpers/DistanceCalculator.php | 22 ++--- .../Helpers/ElevationGainLossCalculator.php | 6 +- src/phpGPX/Helpers/GeoHelper.php | 8 +- src/phpGPX/Helpers/SerializationHelper.php | 26 +++--- src/phpGPX/Models/Bounds.php | 38 ++++---- src/phpGPX/Models/Collection.php | 20 ++--- .../Models/Extensions/AbstractExtension.php | 6 +- .../Models/Extensions/TrackPointExtension.php | 90 +++++++------------ src/phpGPX/Models/Route.php | 4 +- src/phpGPX/Models/Track.php | 4 +- src/phpGPX/Parsers/BoundsParser.php | 14 ++- .../Extensions/TrackPointExtensionParser.php | 24 ++--- .../Tests/Parsers/Bounds/BoundsParserTest.php | 12 +-- .../Parsers/Extension/ExtensionParserTest.php | 12 ++- 15 files changed, 142 insertions(+), 180 deletions(-) diff --git a/src/phpGPX/Helpers/DateTimeHelper.php b/src/phpGPX/Helpers/DateTimeHelper.php index 47518e0..f6108ba 100644 --- a/src/phpGPX/Helpers/DateTimeHelper.php +++ b/src/phpGPX/Helpers/DateTimeHelper.php @@ -20,22 +20,23 @@ class DateTimeHelper * @param Point $point2 * @return bool|int */ - public static function comparePointsByTimestamp(Point $point1, Point $point2) - { + public static function comparePointsByTimestamp(Point $point1, Point $point2): bool|int + { if ($point1->time == $point2->time) { return 0; } return $point1->time > $point2->time; } - /** - * @param $datetime - * @param string $format - * @param string $timezone - * @return null|string - */ - public static function formatDateTime($datetime, $format = 'c', $timezone = 'UTC') - { + /** + * @param $datetime + * @param string $format + * @param string $timezone + * @return null|string + * @throws \Exception + */ + public static function formatDateTime($datetime, string $format = 'c', string $timezone = 'UTC'): ?string + { $formatted = null; if ($datetime instanceof \DateTime) { @@ -46,13 +47,14 @@ public static function formatDateTime($datetime, $format = 'c', $timezone = 'UTC return $formatted; } - /** - * @param $value - * @param string $timezone - * @return \DateTime - */ - public static function parseDateTime($value, $timezone = 'Europe/London') - { + /** + * @param $value + * @param string $timezone + * @return \DateTime + * @throws \Exception + */ + public static function parseDateTime($value, string $timezone = 'Europe/London'): \DateTime + { $timezone = new \DateTimeZone($timezone); $datetime = new \DateTime($value, $timezone); $datetime->setTimezone(new \DateTimeZone(date_default_timezone_get())); diff --git a/src/phpGPX/Helpers/DistanceCalculator.php b/src/phpGPX/Helpers/DistanceCalculator.php index 5a2a51b..a72829e 100644 --- a/src/phpGPX/Helpers/DistanceCalculator.php +++ b/src/phpGPX/Helpers/DistanceCalculator.php @@ -18,7 +18,7 @@ class DistanceCalculator /** * @var Point[] */ - private $points; + private array $points; /** * DistanceCalculator constructor. @@ -29,22 +29,22 @@ public function __construct(array $points) $this->points = $points; } - public function getRawDistance() - { + public function getRawDistance(): float + { return $this->calculate([GeoHelper::class, 'getRawDistance']); } - public function getRealDistance() - { + public function getRealDistance(): float + { return $this->calculate([GeoHelper::class, 'getRealDistance']); } - /** - * @param Point[]|array $points - * @return float - */ - private function calculate($strategy) - { + /** + * @param array $strategy + * @return float + */ + private function calculate(array $strategy): float + { $distance = 0; $pointCount = count($this->points); diff --git a/src/phpGPX/Helpers/ElevationGainLossCalculator.php b/src/phpGPX/Helpers/ElevationGainLossCalculator.php index 7d80e11..3304731 100644 --- a/src/phpGPX/Helpers/ElevationGainLossCalculator.php +++ b/src/phpGPX/Helpers/ElevationGainLossCalculator.php @@ -14,11 +14,11 @@ class ElevationGainLossCalculator { /** - * @param Point[]|array $points + * @param Point[] $points * @return array */ - public static function calculate(array $points) - { + public static function calculate(array $points): array + { $cumulativeElevationGain = 0; $cumulativeElevationLoss = 0; diff --git a/src/phpGPX/Helpers/GeoHelper.php b/src/phpGPX/Helpers/GeoHelper.php index adaabe1..ef37bf5 100644 --- a/src/phpGPX/Helpers/GeoHelper.php +++ b/src/phpGPX/Helpers/GeoHelper.php @@ -24,8 +24,8 @@ abstract class GeoHelper * @param Point $point2 * @return float */ - public static function getRawDistance(Point $point1, Point $point2) - { + public static function getRawDistance(Point $point1, Point $point2): float + { $latFrom = deg2rad($point1->latitude); $lonFrom = deg2rad($point1->longitude); $latTo = deg2rad($point2->latitude); @@ -45,8 +45,8 @@ public static function getRawDistance(Point $point1, Point $point2) * @param Point $point2 * @return float */ - public static function getRealDistance(Point $point1, Point $point2) - { + public static function getRealDistance(Point $point1, Point $point2): float + { $distance = self::getRawDistance($point1, $point2); $elevation1 = $point1->elevation != null ? $point1->elevation : 0; diff --git a/src/phpGPX/Helpers/SerializationHelper.php b/src/phpGPX/Helpers/SerializationHelper.php index 433e950..30cbc69 100644 --- a/src/phpGPX/Helpers/SerializationHelper.php +++ b/src/phpGPX/Helpers/SerializationHelper.php @@ -21,8 +21,8 @@ abstract class SerializationHelper * @param $value * @return int|null */ - public static function integerOrNull($value) - { + public static function integerOrNull($value): ?int + { return is_numeric($value) ? (integer) $value : null; } @@ -31,8 +31,8 @@ public static function integerOrNull($value) * @param $value * @return float|null */ - public static function floatOrNull($value) - { + public static function floatOrNull($value): ?float + { return is_numeric($value) ? (float) $value : null; } @@ -41,8 +41,8 @@ public static function floatOrNull($value) * @param $value * @return null|string */ - public static function stringOrNull($value) - { + public static function stringOrNull($value): ?string + { return is_string($value) ? $value : null; } @@ -51,8 +51,8 @@ public static function stringOrNull($value) * @param Summarizable|Summarizable[] $object * @return array|null */ - public static function serialize($object) - { + public static function serialize(Summarizable|array|null $object): ?array + { if (is_array($object)) { $result = []; foreach ($object as $record) { @@ -62,12 +62,12 @@ public static function serialize($object) $object = null; return $result; } else { - return $object != null ? $object->toArray() : null; + return $object?->toArray(); } } - public static function filterNotNull(array $array) - { + public static function filterNotNull(array $array): array + { foreach ($array as &$item) { if (!is_array($item)) { continue; @@ -76,10 +76,8 @@ public static function filterNotNull(array $array) $item = self::filterNotNull($item); } - $array = array_filter($array, function ($item) { + return array_filter($array, function ($item) { return $item !== null && (!is_array($item) || count($item)); }); - - return $array; } } diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php index 38d5945..f0fd238 100644 --- a/src/phpGPX/Models/Bounds.php +++ b/src/phpGPX/Models/Bounds.php @@ -13,43 +13,47 @@ class Bounds implements Summarizable * Minimal latitude in file. * @var float */ - public $minLatitude; + public float $minLatitude; /** * Minimal longitude in file. * @var float */ - public $minLongitude; + public float $minLongitude; /** * Maximal latitude in file. * @var float */ - public $maxLatitude; + public float $maxLatitude; /** * Maximal longitude in file. * @var float */ - public $maxLongitude; + public float $maxLongitude; - /** - * Bounds constructor. - */ - public function __construct() - { - $this->minLatitude = null; - $this->minLongitude = null; - $this->maxLongitude = null; - $this->maxLatitude = null; - } + /** + * @param float $minLatitude + * @param float $minLongitude + * @param float $maxLatitude + * @param float $maxLongitude + */ + public function __construct(float $minLatitude, float $minLongitude, float $maxLatitude, float $maxLongitude) + { + $this->minLatitude = $minLatitude; + $this->minLongitude = $minLongitude; + $this->maxLatitude = $maxLatitude; + $this->maxLongitude = $maxLongitude; + } - /** + + /** * Serialize object to array * @return array */ - public function toArray() - { + public function toArray(): array + { return [ 'minlat' => $this->minLatitude, 'minlon' => $this->minLongitude, diff --git a/src/phpGPX/Models/Collection.php b/src/phpGPX/Models/Collection.php index 8fabf7a..66b667b 100644 --- a/src/phpGPX/Models/Collection.php +++ b/src/phpGPX/Models/Collection.php @@ -18,62 +18,62 @@ abstract class Collection implements Summarizable, StatsCalculator * An original GPX 1.1 attribute. * @var string|null */ - public $name; + public ?string $name; /** * GPS comment for route. * An original GPX 1.1 attribute. * @var string|null */ - public $comment; + public ?string $comment; /** * Text description of route/track for user. Not sent to GPS. * An original GPX 1.1 attribute. * @var string|null */ - public $description; + public ?string $description; /** * Source of data. Included to give user some idea of reliability and accuracy of data. * An original GPX 1.1 attribute. * @var string|null */ - public $source; + public ?string $source; /** * Links to external information about the route/track. * An original GPX 1.1 attribute. * @var Link[] */ - public $links; + public array $links; /** * GPS route/track number. * An original GPX 1.1 attribute. * @var int|null */ - public $number; + public ?int $number; /** * Type (classification) of route/track. * An original GPX 1.1 attribute. * @var string|null */ - public $type; + public ?string $type; /** * You can add extend GPX by adding your own elements from another schema here. * An original GPX 1.1 attribute. * @var Extensions|null */ - public $extensions; + public ?Extensions $extensions; /** * Objects contains calculated statistics for collection. * @var Stats|null */ - public $stats; + public ?Stats $stats; /** * Collection constructor. @@ -95,5 +95,5 @@ public function __construct() * Return all points in collection. * @return Point[] */ - abstract public function getPoints(); + abstract public function getPoints(): array; } diff --git a/src/phpGPX/Models/Extensions/AbstractExtension.php b/src/phpGPX/Models/Extensions/AbstractExtension.php index fe5ae8b..06ce317 100644 --- a/src/phpGPX/Models/Extensions/AbstractExtension.php +++ b/src/phpGPX/Models/Extensions/AbstractExtension.php @@ -15,20 +15,20 @@ abstract class AbstractExtension implements Summarizable * XML namespace of extension * @var string */ - public $namespace; + public string $namespace; /** * Node name extension. * @var string */ - public $extensionName; + public string $extensionName; /** * AbstractExtension constructor. * @param string $namespace * @param string $extensionName */ - public function __construct($namespace, $extensionName) + public function __construct(string $namespace, string $extensionName) { $this->namespace = $namespace; $this->extensionName = $extensionName; diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php index 53b8dd6..89b2046 100644 --- a/src/phpGPX/Models/Extensions/TrackPointExtension.php +++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php @@ -6,8 +6,6 @@ namespace phpGPX\Models\Extensions; -use phpGPX\Helpers\SerializationHelper; - /** * Class TrackPointExtension * Extension version: v2 @@ -27,75 +25,51 @@ class TrackPointExtension extends AbstractExtension /** * Average temperature value measured in degrees Celsius. - * @var float - */ - public $aTemp; - - /** - * Average temperature value measured in degrees Celsius. - * @deprecated use TrackPointExtension::$aTemp instead. Will be removed in v1.0 - * @see TrackPointExtension::$aTemp - * @var float - */ - public $avgTemperature; + * @var float|null + */ + public ?float $aTemp; /** - * @var float - */ - public $wTemp; + * @var float|null + */ + public ?float $wTemp; /** * Depth in meters. - * @var float - */ - public $depth; - - /** - * Heart rate in beats per minute. - * @deprecated since v1.0RC3, use attribute TrackPointExtension::$hr instead, will be removed in v1.0 - * @see TrackPointExtension::$hr - * @var float - */ - public $heartRate; + * @var float|null + */ + public ?float $depth; /** * Heart rate in beats per minute. * @since v1.0RC3 - * @var float - */ - public $hr; - - /** - * Cadence in revolutions per minute. - * @deprecated since v1.0RC3, use attribute TrackPointExtension::$cad instead, will be removed in v1.0 - * @see TrackPointExtension::$cad - * @var float - */ - public $cadence; + * @var float|null + */ + public ?float $hr; /** * Cadence in revolutions per minute. - * @var float - */ - public $cad; + * @var float|null + */ + public ?float $cad; /** * Speed in meters per second. - * @var float + * @var float|null */ - public $speed; + public ?float $speed; /** * Course. This type contains an angle measured in degrees in a clockwise direction from the true north line. - * @var int - */ - public $course; + * @var int|null + */ + public ?int $course; /** * Bearing. This type contains an angle measured in degrees in a clockwise direction from the true north line. - * @var int + * @var int|null */ - public $bearing; + public ?int $bearing; /** * TrackPointExtension constructor. @@ -109,17 +83,17 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() - { + public function toArray(): array + { return [ - 'aTemp' => SerializationHelper::floatOrNull($this->aTemp), - 'wTemp' => SerializationHelper::floatOrNull($this->wTemp), - 'depth' => SerializationHelper::floatOrNull($this->depth), - 'hr' => SerializationHelper::floatOrNull($this->hr), - 'cad' => SerializationHelper::floatOrNull($this->cad), - 'speed' => SerializationHelper::floatOrNull($this->speed), - 'course' => SerializationHelper::integerOrNull($this->course), - 'bearing' => SerializationHelper::integerOrNull($this->bearing) + 'aTemp' => $this->aTemp ?? null, + 'wTemp' => $this->wTemp ?? null, + 'depth' => $this->depth ?? null, + 'hr' => $this->hr ?? null, + 'cad' => $this->cad ?? null, + 'speed' => $this->speed ?? null, + 'course' => $this->course ?? null, + 'bearing' => $this->bearing ?? null ]; } } diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 599e666..d0ecf2e 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -40,8 +40,8 @@ public function __construct() * Return all points in collection. * @return Point[] */ - public function getPoints() - { + public function getPoints(): array + { /** @var Point[] $points */ $points = []; diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index 271817a..7452cb1 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -37,8 +37,8 @@ public function __construct() * Return all points in collection. * @return Point[] */ - public function getPoints() - { + public function getPoints(): array + { /** @var Point[] $points */ $points = []; diff --git a/src/phpGPX/Parsers/BoundsParser.php b/src/phpGPX/Parsers/BoundsParser.php index ada5ebf..d3657fb 100644 --- a/src/phpGPX/Parsers/BoundsParser.php +++ b/src/phpGPX/Parsers/BoundsParser.php @@ -27,14 +27,12 @@ public static function parse(\SimpleXMLElement $node) return null; } - $bounds = new Bounds(); - - $bounds->minLatitude = isset($node['minlat']) ? (float) $node['minlat'] : null; - $bounds->minLongitude = isset($node['minlon']) ? (float) $node['minlon'] : null; - $bounds->maxLatitude = isset($node['maxlat']) ? (float) $node['maxlat'] : null; - $bounds->maxLongitude = isset($node['maxlon']) ? (float) $node['maxlon'] : null; - - return $bounds; + return new Bounds( + (float) $node['minlat'], + (float) $node['minlon'], + (float) $node['maxlat'], + (float) $node['maxlon'] + ); } /** diff --git a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php index bb5ec42..46dd028 100644 --- a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php +++ b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php @@ -55,25 +55,13 @@ public static function parse($node) $extension = new TrackPointExtension(); foreach (self::$attributeMapper as $key => $attribute) { - $extension->{$attribute['name']} = isset($node->$key) ? $node->$key : null; - if (!is_null($extension->{$attribute['name']})) { - settype($extension->{$attribute['name']}, $attribute['type']); - } + $value = isset($node->$key) ? $node->$key : null; - // Remove in v1.0 - if ($key == 'hr') { - $extension->heartRate = $extension->hr; - } + if (!is_null($value)) { + settype($value, $attribute['type']); + } - // Remove in v1.0 - if ($key == 'cad') { - $extension->cadence = $extension->cad; - } - - // Remove in v1.0 - if ($key == 'atemp') { - $extension->avgTemperature = $extension->aTemp; - } + $extension->{$attribute['name']} = $value; } return $extension; @@ -96,7 +84,7 @@ public static function toXML(TrackPointExtension $extension, \DOMDocument &$docu ]; foreach (self::$attributeMapper as $key => $attribute) { - if (!is_null($extension->{$attribute['name']})) { + if (isset($extension->{$attribute['name']})) { $child = $document->createElement( sprintf("%s:%s", TrackPointExtension::EXTENSION_NAMESPACE_PREFIX, $key), $extension->{$attribute['name']} diff --git a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php index b1f39db..d465bb2 100644 --- a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php +++ b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php @@ -17,12 +17,12 @@ class BoundsParserTest extends TestCase protected function setUp(): void { // Example object - $this->bounds = new Bounds(); - $this->bounds->maxLatitude = 49.090543; - $this->bounds->maxLongitude = 18.886939; - $this->bounds->minLatitude = 49.072489; - $this->bounds->minLongitude = 18.814543; - + $this->bounds = new Bounds( + 49.072489, + 18.814543, + 49.090543, + 18.886939 + ); // Input file $this->file = simplexml_load_file(sprintf("%s/bounds.xml", __DIR__)); } diff --git a/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php b/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php index 8ad7d16..95ebb9d 100644 --- a/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php +++ b/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php @@ -15,10 +15,8 @@ class ExtensionParserTest extends TestCase protected function setUp(): void { $trackpoint = new TrackPointExtension(); - $trackpoint->aTemp = (float) 14; - $trackpoint->avgTemperature = (float) 14; - $trackpoint->hr = (float) 152; - $trackpoint->heartRate = (float) 152; + $trackpoint->aTemp = 14.0; + $trackpoint->hr = 152.0; $this->extensions = new Extensions(); $this->extensions->trackPointExtension = $trackpoint; @@ -39,10 +37,10 @@ public function testParse() { $extensions = ExtensionParser::parse($this->file->extensions); - $this->assertEquals($this->extensions, $extensions); - $this->assertEquals($this->extensions->unsupported, $extensions->unsupported); - $this->assertEquals($this->extensions->trackPointExtension, $extensions->trackPointExtension); + $this->assertEquals( + $this->extensions->trackPointExtension->toArray(), $extensions->trackPointExtension->toArray() + ); $this->assertEquals($this->extensions->toArray(), $extensions->toArray()); } From e36c7aa6b8750b0494e21d91c0e2f2c7d0cd0074 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Thu, 14 Mar 2024 12:33:23 +0100 Subject: [PATCH 07/31] Getting rid of Summarizable --- .codeclimate.yml | 2 +- README.md | 5 +- composer.json | 10 +- docs/_config.yml | 19 - docs/_data/authors.yml | 3 - docs/index.md | 241 - docs/phpGPX-Helpers-DateTimeHelper.md | 75 - docs/phpGPX-Helpers-GeoHelper.md | 50 - docs/phpGPX-Helpers-SerializationHelper.md | 90 - docs/phpGPX-Models-Bounds.md | 93 - docs/phpGPX-Models-Collection.md | 177 - docs/phpGPX-Models-Copyright.md | 83 - docs/phpGPX-Models-Email.md | 72 - ...GPX-Models-Extensions-AbstractExtension.md | 76 - ...X-Models-Extensions-TrackPointExtension.md | 239 - docs/phpGPX-Models-Extensions.md | 61 - docs/phpGPX-Models-GpxFile.md | 159 - docs/phpGPX-Models-Link.md | 82 - docs/phpGPX-Models-Metadata.md | 149 - docs/phpGPX-Models-Person.md | 83 - docs/phpGPX-Models-Point.md | 363 - docs/phpGPX-Models-Route.md | 190 - docs/phpGPX-Models-Segment.md | 98 - docs/phpGPX-Models-Stats.md | 188 - docs/phpGPX-Models-StatsCalculator.md | 33 - docs/phpGPX-Models-Summarizable.md | 33 - docs/phpGPX-Models-Track.md | 190 - docs/phpGPX-Parsers-BoundsParser.md | 70 - docs/phpGPX-Parsers-CopyrightParser.md | 70 - docs/phpGPX-Parsers-EmailParser.md | 70 - docs/phpGPX-Parsers-ExtensionParser.md | 82 - ...rs-Extensions-TrackPointExtensionParser.md | 69 - docs/phpGPX-Parsers-LinkParser.md | 88 - docs/phpGPX-Parsers-MetadataParser.md | 82 - docs/phpGPX-Parsers-PersonParser.md | 70 - docs/phpGPX-Parsers-PointParser.md | 100 - docs/phpGPX-Parsers-RouteParser.md | 100 - docs/phpGPX-Parsers-SegmentParser.md | 88 - docs/phpGPX-Parsers-TrackParser.md | 100 - docs/phpGPX-Parsers-WaypointParser.md | 40 - docs/phpGPX-phpGPX.md | 164 - docs/schemas/GpxExtensionsv3.xsd | 215 + docs/schemas/TrackPointExtensionv1.xsd | 74 + docs/schemas/gpx.xsd | 788 + docs/schemas/gpx_style2.xsd | 428 + .../CreateFileFromScratch.php | 0 {example => examples}/Example.php | 0 {example => examples}/endomondo.gpx | 0 .../output_waypoint_test.gpx | 0 {example => examples}/waypoint_test.gpx | 0 {example => examples}/waypoints_create.php | 0 {example => examples}/waypoints_load.php | 0 phpunit.xml | 44 +- src/phpGPX/Config.php | 82 + src/phpGPX/GpxSerializable.php | 10 + src/phpGPX/Helpers/DateTimeHelper.php | 4 +- src/phpGPX/Models/Bounds.php | 63 +- src/phpGPX/Models/Point.php | 7 + src/phpGPX/Tests/CreateWaypointTest.php | 128 - src/phpGPX/Tests/LoadFileTest.php | 2 +- src/phpGPX/Tests/LoadRouteFileTest.php | 190 - .../Tests/Parsers/Bounds/BoundsParserTest.php | 14 +- src/phpGPX/Tests/Parsers/Bounds/bounds.json | 7 +- src/phpGPX/phpGPX.php | 87 +- tests/fixtures/basic.gpx | 12763 ++++++++++++++++ .../Tests => tests}/fixtures/gps-track.gpx | 0 tests/fixtures/hiking.gpx | 193 + tests/fixtures/minimal.gpx | 124 + .../phpGPX/Tests => tests}/fixtures/route.gpx | 0 .../Tests => tests}/fixtures/timezero.gpx | 0 .../phpGPX}/Helpers/DateTimeHelperTest.php | 3 +- .../phpGPX}/Helpers/GeoHelperTest.php | 3 +- .../Helpers/SerializationHelperTest.php | 3 +- tests/phpGPX/Models/BoundsTest.php | 22 + 74 files changed, 14788 insertions(+), 4523 deletions(-) delete mode 100644 docs/_config.yml delete mode 100644 docs/_data/authors.yml delete mode 100644 docs/index.md delete mode 100644 docs/phpGPX-Helpers-DateTimeHelper.md delete mode 100644 docs/phpGPX-Helpers-GeoHelper.md delete mode 100644 docs/phpGPX-Helpers-SerializationHelper.md delete mode 100644 docs/phpGPX-Models-Bounds.md delete mode 100644 docs/phpGPX-Models-Collection.md delete mode 100644 docs/phpGPX-Models-Copyright.md delete mode 100644 docs/phpGPX-Models-Email.md delete mode 100644 docs/phpGPX-Models-Extensions-AbstractExtension.md delete mode 100644 docs/phpGPX-Models-Extensions-TrackPointExtension.md delete mode 100644 docs/phpGPX-Models-Extensions.md delete mode 100644 docs/phpGPX-Models-GpxFile.md delete mode 100644 docs/phpGPX-Models-Link.md delete mode 100644 docs/phpGPX-Models-Metadata.md delete mode 100644 docs/phpGPX-Models-Person.md delete mode 100644 docs/phpGPX-Models-Point.md delete mode 100644 docs/phpGPX-Models-Route.md delete mode 100644 docs/phpGPX-Models-Segment.md delete mode 100644 docs/phpGPX-Models-Stats.md delete mode 100644 docs/phpGPX-Models-StatsCalculator.md delete mode 100644 docs/phpGPX-Models-Summarizable.md delete mode 100644 docs/phpGPX-Models-Track.md delete mode 100644 docs/phpGPX-Parsers-BoundsParser.md delete mode 100644 docs/phpGPX-Parsers-CopyrightParser.md delete mode 100644 docs/phpGPX-Parsers-EmailParser.md delete mode 100644 docs/phpGPX-Parsers-ExtensionParser.md delete mode 100644 docs/phpGPX-Parsers-Extensions-TrackPointExtensionParser.md delete mode 100644 docs/phpGPX-Parsers-LinkParser.md delete mode 100644 docs/phpGPX-Parsers-MetadataParser.md delete mode 100644 docs/phpGPX-Parsers-PersonParser.md delete mode 100644 docs/phpGPX-Parsers-PointParser.md delete mode 100644 docs/phpGPX-Parsers-RouteParser.md delete mode 100644 docs/phpGPX-Parsers-SegmentParser.md delete mode 100644 docs/phpGPX-Parsers-TrackParser.md delete mode 100644 docs/phpGPX-Parsers-WaypointParser.md delete mode 100644 docs/phpGPX-phpGPX.md create mode 100644 docs/schemas/GpxExtensionsv3.xsd create mode 100644 docs/schemas/TrackPointExtensionv1.xsd create mode 100644 docs/schemas/gpx.xsd create mode 100644 docs/schemas/gpx_style2.xsd rename {example => examples}/CreateFileFromScratch.php (100%) rename {example => examples}/Example.php (100%) rename {example => examples}/endomondo.gpx (100%) rename {example => examples}/output_waypoint_test.gpx (100%) rename {example => examples}/waypoint_test.gpx (100%) rename {example => examples}/waypoints_create.php (100%) rename {example => examples}/waypoints_load.php (100%) create mode 100644 src/phpGPX/Config.php create mode 100644 src/phpGPX/GpxSerializable.php delete mode 100644 src/phpGPX/Tests/CreateWaypointTest.php delete mode 100644 src/phpGPX/Tests/LoadRouteFileTest.php create mode 100644 tests/fixtures/basic.gpx rename {src/phpGPX/Tests => tests}/fixtures/gps-track.gpx (100%) create mode 100644 tests/fixtures/hiking.gpx create mode 100644 tests/fixtures/minimal.gpx rename {src/phpGPX/Tests => tests}/fixtures/route.gpx (100%) rename {src/phpGPX/Tests => tests}/fixtures/timezero.gpx (100%) rename {src/phpGPX/Tests => tests/phpGPX}/Helpers/DateTimeHelperTest.php (97%) rename {src/phpGPX/Tests => tests/phpGPX}/Helpers/GeoHelperTest.php (96%) rename {src/phpGPX/Tests => tests/phpGPX}/Helpers/SerializationHelperTest.php (97%) create mode 100644 tests/phpGPX/Models/BoundsTest.php diff --git a/.codeclimate.yml b/.codeclimate.yml index 2c7b913..c3b72fd 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -13,4 +13,4 @@ ratings: - "**.php" exclude_paths: - docs/* - - example/* \ No newline at end of file + - examples/* \ No newline at end of file diff --git a/README.md b/README.md index c95a293..7b283f3 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,9 @@ [![Code Climate](https://codeclimate.com/github/Sibyx/phpGPX/badges/gpa.svg)](https://codeclimate.com/github/Sibyx/phpGPX) [![Latest development](https://img.shields.io/packagist/vpre/sibyx/phpgpx.svg)](https://packagist.org/packages/sibyx/phpgpx) [![Packagist downloads](https://img.shields.io/packagist/dm/sibyx/phpgpx.svg)](https://packagist.org/packages/sibyx/phpgpx) -[![Gitter](https://img.shields.io/gitter/room/phpgpx/phpgpx.svg)](https://gitter.im/phpGPX/Lobby) Simple library written in PHP for reading and creating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format). -[Documentation](https://sibyx.github.io/phpGPX/) is available using GitHub pages generated by Jekyll. Contribution and feedback is welcome! Please check the issues for TODO. I will be happy every feature or pull request. @@ -277,7 +275,8 @@ public static $APPLY_DISTANCE_SMOOTHING = false; public static $DISTANCE_SMOOTHING_THRESHOLD = 2; ``` -I wrote this library as part of my job in [Backbone s.r.o.](https://www.backbone.sk/en/). +This library started as part of my job at [BACKBONE, s.r.o.](https://www.backbone.sk/en/). +Thank you very much for their support! ## License diff --git a/composer.json b/composer.json index 6e6d162..1d43ebc 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "sibyx/phpgpx", "type": "library", "version": "1.3.0", - "description": "A simple PHP library for GPX import/export", + "description": "A simple PHP library for GPX manipulation", "minimum-stability": "stable", "keywords": [ "gpx", @@ -27,13 +27,13 @@ "ext-dom": "*" }, "require-dev": { - "phpunit/phpunit": "^10.2.6", - "friendsofphp/php-cs-fixer": "^v3.22.0" + "phpunit/phpunit": "^10.5.5", + "friendsofphp/php-cs-fixer": "^v3.43.1" }, "autoload": { - "psr-4": { "phpGPX\\": "src/phpGPX/" } + "psr-4": { "phpGPX\\": "src/phpGPX" } }, "autoload-dev": { - "psr-4": { "phpGPX\\Tests\\": "tests/" } + "psr-4": { "phpGPX\\": "tests/phpGPX" } } } diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 526423e..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,19 +0,0 @@ -theme: jekyll-theme-cayman -author: sibyx -title: phpGPX library -description: Simple library for reading and creating GPX files written in PHP. -gems: - - jekyll-seo-tag - - jekyll-analytics - - jekyll-sitemap -social: - name: Jakub Dubec - links: - - https://www.facebook.com/dubecj - - https://github.com/Sibyx - - https://keybase.io/jakubdubec - -jekyll_analytics: - GoogleAnalytics: - id: UA-99333937-1 - anonymizeIp: false \ No newline at end of file diff --git a/docs/_data/authors.yml b/docs/_data/authors.yml deleted file mode 100644 index e27639c..0000000 --- a/docs/_data/authors.yml +++ /dev/null @@ -1,3 +0,0 @@ -sibyx: - picture: https://www.gravatar.com/avatar/c08cc3a6b17177a4bd7045566b0d772a?s=500 - twitter: jakubdubec \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 7e7626c..0000000 --- a/docs/index.md +++ /dev/null @@ -1,241 +0,0 @@ -Simple library written in PHP for reading and creating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format). - -Library is currently marked as Release Candidate but is already used in production on several projects without any -problems. [Documentation](https://sibyx.github.io/phpGPX/) is available using GitHub pages generated by Jekyll. - -Contribution and feedback is welcome! Please check the issues for TODO. I will be happy every feature or pull request. - -## Features - - - Full support of [official specification](http://www.topografix.com/GPX/1/1/). - - Statistics calculation. - - Extensions. - - JSON & XML & PHP Array output. - -### Supported Extensions - - Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd): http://www.garmin.com/xmlschemas/TrackPointExtension/v1 - -### Stats calculation - - - (Smoothed) Distance (m) - - Average speed (m/s) - - Average pace (s/km) - - Min / max altitude (m) - - (Smoothed) Elevation gain / loss (m) - - Start / end (DateTime object) - - Duration (seconds) - -## Examples - -### Open GPX file and load basic stats -```php -load('example.gpx'); - -foreach ($file->tracks as $track) -{ - // Statistics for whole track - $track->stats->toArray(); - - foreach ($track->segments as $segment) - { - // Statistics for segment of track - $segment->stats->toArray(); - } -} -``` - -### Writing to file -```php -load('example.gpx'); - -// XML -$file->save('output.gpx', phpGPX::XML_FORMAT); - -//JSON -$file->save('output.json', phpGPX::JSON_FORMAT); -``` - -### Creating file from scratch -```php - 9.860624216140083, - 'latitude' => 54.9328621088893, - 'elevation' => 0, - 'time' => new \DateTime("+ 1 MINUTE") - ], - [ - 'latitude' => 54.83293237320851, - 'longitude' => 9.76092208681491, - 'elevation' => 10.0, - 'time' => new \DateTime("+ 2 MINUTE") - ], - [ - 'latitude' => 54.73327743521187, - 'longitude' => 9.66187816543752, - 'elevation' => 42.42, - 'time' => new \DateTime("+ 3 MINUTE") - ], - [ - 'latitude' => 54.63342326167919, - 'longitude' => 9.562439849679859, - 'elevation' => 12, - 'time' => new \DateTime("+ 4 MINUTE") - ] -]; - -// Creating sample link object for metadata -$link = new Link(); -$link->href = "https://sibyx.github.io/phpgpx"; -$link->text = 'phpGPX Docs'; - -// GpxFile contains data and handles serialization of objects -$gpx_file = new GpxFile(); - -// Creating sample Metadata object -$gpx_file->metadata = new Metadata(); - -// Time attribute is always \DateTime object! -$gpx_file->metadata->time = new \DateTime(); - -// Description of GPX file -$gpx_file->metadata->description = "My pretty awesome GPX file, created using phpGPX library!"; - -// Adding link created before to links array of metadata -// Metadata of GPX file can contain more than one link -$gpx_file->metadata->links[] = $link; - -// Creating track -$track = new Track(); - -// Name of track -$track->name = "Some random points in logical order. Input array should be already ordered!"; - -// Type of data stored in track -$track->type = 'RUN'; - -// Source of GPS coordinates -$track->source = "MySpecificGarminDevice"; - -// Creating Track segment -$segment = new Segment(); - - -foreach ($sample_data as $sample_point) -{ - // Creating trackpoint - $point = new Point(Point::TRACKPOINT); - $point->latitude = $sample_point['latitude']; - $point->longitude = $sample_point['longitude']; - $point->elevation = $sample_point['elevation']; - $point->time = $sample_point['time']; - - $segment->points[] = $point; -} - -// Add segment to segment array of track -$track->segments[] = $segment; - -// Recalculate stats based on received data -$track->recalculateStats(); - -// Add track to file -$gpx_file->tracks[] = $track; - -// GPX output -$gpx_file->save('CreatingFileFromScratchExample.gpx', \phpGPX\phpGPX::XML_FORMAT); - -// Serialized data as JSON -$gpx_file->save('CreatingFileFromScratchExample.json', \phpGPX\phpGPX::JSON_FORMAT); - -// Direct GPX output to browser - -header("Content-Type: application/gpx+xml"); -header("Content-Disposition: attachment; filename=CreatingFileFromScratchExample.gpx"); - -echo $gpx_file->toXML()->saveXML(); -exit(); -``` - -## Installation - -You can easily install phpGPX library with composer. There is no stable release yet, so please use release candidates. - -``` -composer require sibyx/phpgpx:@RC -``` - -## API Documentation - -* phpGPX - * phpGPX\Parsers - * [EmailParser](phpGPX-Parsers-EmailParser.md) - * [CopyrightParser](phpGPX-Parsers-CopyrightParser.md) - * [SegmentParser](phpGPX-Parsers-SegmentParser.md) - * [LinkParser](phpGPX-Parsers-LinkParser.md) - * [TrackParser](phpGPX-Parsers-TrackParser.md) - * [WaypointParser](phpGPX-Parsers-WaypointParser.md) - * [PersonParser](phpGPX-Parsers-PersonParser.md) - * [MetadataParser](phpGPX-Parsers-MetadataParser.md) - * phpGPX\Parsers\Extensions - * [TrackPointExtensionParser](phpGPX-Parsers-Extensions-TrackPointExtensionParser.md) - * [PointParser](phpGPX-Parsers-PointParser.md) - * [RouteParser](phpGPX-Parsers-RouteParser.md) - * [ExtensionParser](phpGPX-Parsers-ExtensionParser.md) - * [BoundsParser](phpGPX-Parsers-BoundsParser.md) - * phpGPX\Helpers - * [DateTimeHelper](phpGPX-Helpers-DateTimeHelper.md) - * [GeoHelper](phpGPX-Helpers-GeoHelper.md) - * [SerializationHelper](phpGPX-Helpers-SerializationHelper.md) - * phpGPX\Models - * [Point](phpGPX-Models-Point.md) - * [Extensions](phpGPX-Models-Extensions.md) - * [AbstractExtension](phpGPX-Models-Extensions-AbstractExtension.md) - * [TrackPointExtension](phpGPX-Models-Extensions-TrackPointExtension.md) - * [Stats](phpGPX-Models-Stats.md) - * [Route](phpGPX-Models-Route.md) - * [Email](phpGPX-Models-Email.md) - * [Collection](phpGPX-Models-Collection.md) - * [StatsCalculator](phpGPX-Models-StatsCalculator.md) - * [Copyright](phpGPX-Models-Copyright.md) - * [Summarizable](phpGPX-Models-Summarizable.md) - * [Segment](phpGPX-Models-Segment.md) - * [Person](phpGPX-Models-Person.md) - * [Link](phpGPX-Models-Link.md) - * [GpxFile](phpGPX-Models-GpxFile.md) - * [Bounds](phpGPX-Models-Bounds.md) - * [Metadata](phpGPX-Models-Metadata.md) - * [Track](phpGPX-Models-Track.md) - * [phpGPX](phpGPX-phpGPX.md) - -## Contributors - - - [Jakub Dubec](https://github.com/Sibyx) - Initial works, maintenance - - [Lukasz Lewandowski](https://github.com/luklewluk) - -I wrote this library as part of my job in [Backbone s.r.o.](https://www.backbone.sk/en/). - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/phpGPX-Helpers-DateTimeHelper.md b/docs/phpGPX-Helpers-DateTimeHelper.md deleted file mode 100644 index afc568d..0000000 --- a/docs/phpGPX-Helpers-DateTimeHelper.md +++ /dev/null @@ -1,75 +0,0 @@ -phpGPX\Helpers\DateTimeHelper -=============== - -Class DateTimeHelper - - - - -* Class name: DateTimeHelper -* Namespace: phpGPX\Helpers - - - - - - - -Methods -------- - - -### comparePointsByTimestamp - - boolean|integer phpGPX\Helpers\DateTimeHelper::comparePointsByTimestamp(\phpGPX\Models\Point $point1, \phpGPX\Models\Point $point2) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $point1 **[phpGPX\Models\Point](phpGPX-Models-Point.md)** -* $point2 **[phpGPX\Models\Point](phpGPX-Models-Point.md)** - - - -### formatDateTime - - null|string phpGPX\Helpers\DateTimeHelper::formatDateTime($datetime, string $format, string $timezone) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $datetime **mixed** -* $format **string** -* $timezone **string** - - - -### parseDateTime - - \DateTime phpGPX\Helpers\DateTimeHelper::parseDateTime($value, string $timezone) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $value **mixed** -* $timezone **string** - - diff --git a/docs/phpGPX-Helpers-GeoHelper.md b/docs/phpGPX-Helpers-GeoHelper.md deleted file mode 100644 index 3b3ff91..0000000 --- a/docs/phpGPX-Helpers-GeoHelper.md +++ /dev/null @@ -1,50 +0,0 @@ -phpGPX\Helpers\GeoHelper -=============== - -Class GeoHelper -Geolocation methods. - - - - -* Class name: GeoHelper -* Namespace: phpGPX\Helpers -* This is an **abstract** class - - - -Constants ----------- - - -### EARTH_RADIUS - - const EARTH_RADIUS = 6371000 - - - - - - - -Methods -------- - - -### getDistance - - float phpGPX\Helpers\GeoHelper::getDistance(\phpGPX\Models\Point $point1, \phpGPX\Models\Point $point2) - -Returns distance in meters between two Points according to GPX coordinates. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $point1 **[phpGPX\Models\Point](phpGPX-Models-Point.md)** -* $point2 **[phpGPX\Models\Point](phpGPX-Models-Point.md)** - - diff --git a/docs/phpGPX-Helpers-SerializationHelper.md b/docs/phpGPX-Helpers-SerializationHelper.md deleted file mode 100644 index 568b0c4..0000000 --- a/docs/phpGPX-Helpers-SerializationHelper.md +++ /dev/null @@ -1,90 +0,0 @@ -phpGPX\Helpers\SerializationHelper -=============== - -Class SerializationHelper -Contains basic serialization helpers used in summary() methods. - - - - -* Class name: SerializationHelper -* Namespace: phpGPX\Helpers -* This is an **abstract** class - - - - - - - -Methods -------- - - -### integerOrNull - - integer|null phpGPX\Helpers\SerializationHelper::integerOrNull($value) - -Returns integer or null. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $value **mixed** - - - -### floatOrNull - - float|null phpGPX\Helpers\SerializationHelper::floatOrNull($value) - -Returns float or null. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $value **mixed** - - - -### stringOrNull - - null|string phpGPX\Helpers\SerializationHelper::stringOrNull($value) - -Returns string or null - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $value **mixed** - - - -### serialize - - array|null phpGPX\Helpers\SerializationHelper::serialize(\phpGPX\Models\Summarizable|array $object) - -Recursively traverse Summarizable objects and returns their array representation according summary() method. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $object **[phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md)|array<mixed,\phpGPX\Models\Summarizable>** - - diff --git a/docs/phpGPX-Models-Bounds.md b/docs/phpGPX-Models-Bounds.md deleted file mode 100644 index ceef71d..0000000 --- a/docs/phpGPX-Models-Bounds.md +++ /dev/null @@ -1,93 +0,0 @@ -phpGPX\Models\Bounds -=============== - - - - - - -* Class name: Bounds -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $minLatitude - - public float $minLatitude - -Minimal latitude in file. - - - -* Visibility: **public** - - -### $minLongitude - - public float $minLongitude - -Minimal longitude in file. - - - -* Visibility: **public** - - -### $maxLatitude - - public float $maxLatitude - -Maximal latitude in file. - - - -* Visibility: **public** - - -### $maxLongitude - - public float $maxLongitude - -Maximal longitude in file. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Bounds::__construct() - -Bounds constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Collection.md b/docs/phpGPX-Models-Collection.md deleted file mode 100644 index a9c8067..0000000 --- a/docs/phpGPX-Models-Collection.md +++ /dev/null @@ -1,177 +0,0 @@ -phpGPX\Models\Collection -=============== - -Class Collection - - - - -* Class name: Collection -* Namespace: phpGPX\Models -* This is an **abstract** class -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md), [phpGPX\Models\StatsCalculator](phpGPX-Models-StatsCalculator.md) - - - - -Properties ----------- - - -### $name - - public string $name - -GPS name of route / track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $comment - - public string $comment - -GPS comment for route. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $description - - public string $description - -Text description of route/track for user. Not sent to GPS. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $source - - public string $source - -Source of data. Included to give user some idea of reliability and accuracy of data. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $links - - public array $links - -Links to external information about the route/track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $number - - public integer $number - -GPS route/track number. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $type - - public string $type - -Type (classification) of route/track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - -You can add extend GPX by adding your own elements from another schema here. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $stats - - public \phpGPX\Models\Stats $stats - -Objects contains calculated statistics for collection. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Collection::__construct() - -Collection constructor. - - - -* Visibility: **public** - - - - -### getPoints - - array phpGPX\Models\Collection::getPoints() - -Return all points in collection. - - - -* Visibility: **public** -* This method is **abstract**. - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -### recalculateStats - - void phpGPX\Models\StatsCalculator::recalculateStats() - -Recalculate stats objects. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\StatsCalculator](phpGPX-Models-StatsCalculator.md) - - - diff --git a/docs/phpGPX-Models-Copyright.md b/docs/phpGPX-Models-Copyright.md deleted file mode 100644 index 3d8652a..0000000 --- a/docs/phpGPX-Models-Copyright.md +++ /dev/null @@ -1,83 +0,0 @@ -phpGPX\Models\Copyright -=============== - -Class Copyright -Information about the copyright holder and any license governing use of this file. - -By linking to an appropriate license, you may place your data into the public domain or grant additional usage rights. - - -* Class name: Copyright -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $author - - public string $author - -Copyright holder (TopoSoft, Inc.) - - - -* Visibility: **public** - - -### $year - - public string $year - -Year of copyright. - - - -* Visibility: **public** - - -### $license - - public string $license - -Link to external file containing license text. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Copyright::__construct() - -Copyright constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Email.md b/docs/phpGPX-Models-Email.md deleted file mode 100644 index 9f23c2c..0000000 --- a/docs/phpGPX-Models-Email.md +++ /dev/null @@ -1,72 +0,0 @@ -phpGPX\Models\Email -=============== - -Class Email -An email address. Broken into two parts (id and domain) to help prevent email harvesting. - - - - -* Class name: Email -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $id - - public string $id - -Id half of email address (jakub.dubec) - - - -* Visibility: **public** - - -### $domain - - public string $domain - -Domain half of email address (gmail.com) - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Email::__construct() - -Email constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Extensions-AbstractExtension.md b/docs/phpGPX-Models-Extensions-AbstractExtension.md deleted file mode 100644 index e650b84..0000000 --- a/docs/phpGPX-Models-Extensions-AbstractExtension.md +++ /dev/null @@ -1,76 +0,0 @@ -phpGPX\Models\Extensions\AbstractExtension -=============== - - - - - - -* Class name: AbstractExtension -* Namespace: phpGPX\Models\Extensions -* This is an **abstract** class -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $namespace - - public string $namespace - -XML namespace of extension - - - -* Visibility: **public** - - -### $extensionName - - public string $extensionName - -Node name extension. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Extensions\AbstractExtension::__construct(string $namespace, string $extensionName) - -AbstractExtension constructor. - - - -* Visibility: **public** - - -#### Arguments -* $namespace **string** -* $extensionName **string** - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Extensions-TrackPointExtension.md b/docs/phpGPX-Models-Extensions-TrackPointExtension.md deleted file mode 100644 index a13c319..0000000 --- a/docs/phpGPX-Models-Extensions-TrackPointExtension.md +++ /dev/null @@ -1,239 +0,0 @@ -phpGPX\Models\Extensions\TrackPointExtension -=============== - -Class TrackPointExtension -Extension version: v2 -Based on namespace: http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd - - - - -* Class name: TrackPointExtension -* Namespace: phpGPX\Models\Extensions -* Parent class: [phpGPX\Models\Extensions\AbstractExtension](phpGPX-Models-Extensions-AbstractExtension.md) - - - -Constants ----------- - - -### EXTENSION_V1_NAMESPACE - - const EXTENSION_V1_NAMESPACE = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1' - - - - - -### EXTENSION_V1_NAMESPACE_XSD - - const EXTENSION_V1_NAMESPACE_XSD = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd' - - - - - -### EXTENSION_NAMESPACE - - const EXTENSION_NAMESPACE = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2' - - - - - -### EXTENSION_NAMESPACE_XSD - - const EXTENSION_NAMESPACE_XSD = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd' - - - - - -### EXTENSION_NAME - - const EXTENSION_NAME = 'TrackPointExtension' - - - - - -### EXTENSION_NAMESPACE_PREFIX - - const EXTENSION_NAMESPACE_PREFIX = 'gpxtpx' - - - - - -Properties ----------- - - -### $aTemp - - public float $aTemp - -Average temperature value measured in degrees Celsius. - - - -* Visibility: **public** - - -### $wTemp - - public float $wTemp - - - - - -* Visibility: **public** - - -### $depth - - public float $depth - -Depth in meters. - - - -* Visibility: **public** - - -### $heartRate - - public float $heartRate - -Heart rate in beats per minute. - - - -* Visibility: **public** - - -### $hr - - public float $hr - -Heart rate in beats per minute. - - - -* Visibility: **public** - - -### $cadence - - public float $cadence - -Cadence in revolutions per minute. - - - -* Visibility: **public** - - -### $cad - - public float $cad - -Cadence in revolutions per minute. - - - -* Visibility: **public** - - -### $speed - - public float $speed - -Speed in meters per second. - - - -* Visibility: **public** - - -### $course - - public integer $course - -Course. This type contains an angle measured in degrees in a clockwise direction from the true north line. - - - -* Visibility: **public** - - -### $bearing - - public integer $bearing - -Bearing. This type contains an angle measured in degrees in a clockwise direction from the true north line. - - - -* Visibility: **public** - - -### $namespace - - public string $namespace - -XML namespace of extension - - - -* Visibility: **public** - - -### $extensionName - - public string $extensionName - -Node name extension. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Extensions\AbstractExtension::__construct(string $namespace, string $extensionName) - -AbstractExtension constructor. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Extensions\AbstractExtension](phpGPX-Models-Extensions-AbstractExtension.md) - - -#### Arguments -* $namespace **string** -* $extensionName **string** - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Extensions.md b/docs/phpGPX-Models-Extensions.md deleted file mode 100644 index ee1fcd1..0000000 --- a/docs/phpGPX-Models-Extensions.md +++ /dev/null @@ -1,61 +0,0 @@ -phpGPX\Models\Extensions -=============== - -Class Extensions -TODO: http://www.garmin.com/xmlschemas/GpxExtensions/v3 - - - - -* Class name: Extensions -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $trackPointExtension - - public \phpGPX\Models\Extensions\TrackPointExtension $trackPointExtension - -GPX Garmin TrackPointExtension v1 - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Extensions::__construct() - -Extensions constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-GpxFile.md b/docs/phpGPX-Models-GpxFile.md deleted file mode 100644 index 9ed1125..0000000 --- a/docs/phpGPX-Models-GpxFile.md +++ /dev/null @@ -1,159 +0,0 @@ -phpGPX\Models\GpxFile -=============== - -Class GpxFile -Representation of GPX file. - - - - -* Class name: GpxFile -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $waypoints - - public array $waypoints - -A list of waypoints. - - - -* Visibility: **public** - - -### $routes - - public array $routes - -A list of routes. - - - -* Visibility: **public** - - -### $tracks - - public array $tracks - -A list of tracks. - - - -* Visibility: **public** - - -### $metadata - - public \phpGPX\Models\Metadata $metadata - -Metadata about the file. - -The original GPX 1.1 attribute. - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - - - - - -* Visibility: **public** - - -### $creator - - public string $creator - -Creator of GPX file. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\GpxFile::__construct() - -GpxFile constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -### toJSON - - string phpGPX\Models\GpxFile::toJSON() - -Return JSON representation of GPX file with statistics. - - - -* Visibility: **public** - - - - -### toXML - - \DOMDocument phpGPX\Models\GpxFile::toXML() - -Create XML representation of GPX file. - - - -* Visibility: **public** - - - - -### save - - mixed phpGPX\Models\GpxFile::save(string $path, string $format) - -Save data to file according to selected format. - - - -* Visibility: **public** - - -#### Arguments -* $path **string** -* $format **string** - - diff --git a/docs/phpGPX-Models-Link.md b/docs/phpGPX-Models-Link.md deleted file mode 100644 index ed23905..0000000 --- a/docs/phpGPX-Models-Link.md +++ /dev/null @@ -1,82 +0,0 @@ -phpGPX\Models\Link -=============== - -Class Link according to GPX 1.1 specification. - -A link to an external resource (Web page, digital photo, video clip, etc) with additional information. - - -* Class name: Link -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $href - - public string $href - -URL of hyperlink. - - - -* Visibility: **public** - - -### $text - - public string $text - -Text of hyperlink. - - - -* Visibility: **public** - - -### $type - - public string $type - -Mime type of content (image/jpeg) - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Link::__construct() - -Link constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Metadata.md b/docs/phpGPX-Models-Metadata.md deleted file mode 100644 index 9b9b0e0..0000000 --- a/docs/phpGPX-Models-Metadata.md +++ /dev/null @@ -1,149 +0,0 @@ -phpGPX\Models\Metadata -=============== - -Class Metadata -Information about the GPX file, author, and copyright restrictions goes in the metadata section. - -Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. - - -* Class name: Metadata -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $name - - public string $name - -The name of the GPX file. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $description - - public string $description - -A description of the contents of the GPX file. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $author - - public \phpGPX\Models\Person $author - -The person or organization who created the GPX file. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $copyright - - public \phpGPX\Models\Copyright $copyright - -Copyright and license information governing use of the file. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $links - - public array $links - -Original GPX 1.1 attribute. - - - -* Visibility: **public** - - -### $time - - public \DateTime $time - -Date of GPX creation - - - -* Visibility: **public** - - -### $keywords - - public string $keywords - -Keywords associated with the file. Search engines or databases can use this information to classify the data. - - - -* Visibility: **public** - - -### $bounds - - public \phpGPX\Models\Bounds $bounds - -Minimum and maximum coordinates which describe the extent of the coordinates in the file. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - -Extensions. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Metadata::__construct() - -Metadata constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Person.md b/docs/phpGPX-Models-Person.md deleted file mode 100644 index 4aeb55a..0000000 --- a/docs/phpGPX-Models-Person.md +++ /dev/null @@ -1,83 +0,0 @@ -phpGPX\Models\Person -=============== - -Class Person -A person or organisation - - - - -* Class name: Person -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $name - - public string $name - -Name of person or organization. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $email - - public \phpGPX\Models\Email $email - -E-mail address. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $link - - public \phpGPX\Models\Link $link - -Link to Web site or other external information about person. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Person::__construct() - -Person constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Point.md b/docs/phpGPX-Models-Point.md deleted file mode 100644 index c05957f..0000000 --- a/docs/phpGPX-Models-Point.md +++ /dev/null @@ -1,363 +0,0 @@ -phpGPX\Models\Point -=============== - -Class Point -GPX point representation according to GPX 1.1 specification. - - - - -* Class name: Point -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - -Constants ----------- - - -### WAYPOINT - - const WAYPOINT = 'waypoint' - - - - - -### TRACKPOINT - - const TRACKPOINT = 'track' - - - - - -### ROUTEPOINT - - const ROUTEPOINT = 'route' - - - - - -Properties ----------- - - -### $latitude - - public float $latitude - -The latitude of the point. Decimal degrees, WGS84 datum. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $longitude - - public float $longitude - -The longitude of the point. Decimal degrees, WGS84 datum. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $elevation - - public float $elevation - -Elevation (in meters) of the point. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $time - - public \DateTime $time - -Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! -Fractional seconds are allowed for millisecond timing in tracklogs. - - - -* Visibility: **public** - - -### $magVar - - public float $magVar - -Magnetic variation (in degrees) at the point -Original GPX 1.1 attribute. - - - -* Visibility: **public** - - -### $geoidHeight - - public float $geoidHeight - -Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $name - - public string $name - -The GPS name of the waypoint. This field will be transferred to and from the GPS. - -GPX does not place restrictions on the length of this field or the characters contained in it. -It is up to the receiving application to validate the field before sending it to the GPS. -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $comment - - public string $comment - -GPS waypoint comment. Sent to GPS as comment. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $description - - public string $description - -A text description of the element. Holds additional information about the element intended for the user, not the GPS. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $source - - public string $source - -Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $links - - public array $links - -Link to additional information about the waypoint. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $symbol - - public string $symbol - -Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. - -If the GPS abbreviates words, spell them out. -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $type - - public string $type - -Type (classification) of the waypoint. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $fix - - public string $fix - -Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used -Possible values: {'none'|'2d'|'3d'|'dgps'|'pps'} -Original GPX 1.1 attribute. - - - -* Visibility: **public** - - -### $satellitesNumber - - public integer $satellitesNumber - -Number of satellites used to calculate the GPX fix. Always positive value. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $hdop - - public float $hdop - -Horizontal dilution of precision. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $vdop - - public float $vdop - -Vertical dilution of precision. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $pdop - - public float $pdop - -Position dilution of precision. - -Original GPX 1.1 attribute - -* Visibility: **public** - - -### $ageOfGpsData - - public integer $ageOfGpsData - -Number of seconds since last DGPS update. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $dgpsid - - public integer $dgpsid - -ID of DGPS station used in differential correction. - -Original GPX 1.1 attribute. - -* Visibility: **public** - - -### $difference - - public float $difference - -Difference in in distance (in meters) between last point. - -Value is created by phpGPX library. - -* Visibility: **public** - - -### $distance - - public float $distance - -Distance from collection start in meters. - -Value is created by phpGPX library. - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - -Objects stores GPX extensions from another namespaces. - - - -* Visibility: **public** - - -### $pointType - - private string $pointType - -Type of the point (parent collation type (ROUTE|WAYPOINT|TRACK)) - - - -* Visibility: **private** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Point::__construct(string $pointType) - -Point constructor. - - - -* Visibility: **public** - - -#### Arguments -* $pointType **string** - - - -### getPointType - - string phpGPX\Models\Point::getPointType() - -Return point type (ROUTE|TRACK|WAYPOINT) - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-Route.md b/docs/phpGPX-Models-Route.md deleted file mode 100644 index 629a14c..0000000 --- a/docs/phpGPX-Models-Route.md +++ /dev/null @@ -1,190 +0,0 @@ -phpGPX\Models\Route -=============== - -Class Route - - - - -* Class name: Route -* Namespace: phpGPX\Models -* Parent class: [phpGPX\Models\Collection](phpGPX-Models-Collection.md) - - - - - -Properties ----------- - - -### $points - - public array $points - -A list of route points. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $name - - public string $name - -GPS name of route / track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $comment - - public string $comment - -GPS comment for route. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $description - - public string $description - -Text description of route/track for user. Not sent to GPS. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $source - - public string $source - -Source of data. Included to give user some idea of reliability and accuracy of data. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $links - - public array $links - -Links to external information about the route/track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $number - - public integer $number - -GPS route/track number. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $type - - public string $type - -Type (classification) of route/track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - -You can add extend GPX by adding your own elements from another schema here. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $stats - - public \phpGPX\Models\Stats $stats - -Objects contains calculated statistics for collection. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Collection::__construct() - -Collection constructor. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Collection](phpGPX-Models-Collection.md) - - - - -### getPoints - - array phpGPX\Models\Collection::getPoints() - -Return all points in collection. - - - -* Visibility: **public** -* This method is **abstract**. -* This method is defined by [phpGPX\Models\Collection](phpGPX-Models-Collection.md) - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -### recalculateStats - - void phpGPX\Models\StatsCalculator::recalculateStats() - -Recalculate stats objects. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\StatsCalculator](phpGPX-Models-StatsCalculator.md) - - - diff --git a/docs/phpGPX-Models-Segment.md b/docs/phpGPX-Models-Segment.md deleted file mode 100644 index 3550e21..0000000 --- a/docs/phpGPX-Models-Segment.md +++ /dev/null @@ -1,98 +0,0 @@ -phpGPX\Models\Segment -=============== - -Class Segment -A Track Segment holds a list of Track Points which are logically connected in order. - -To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, -start a new Track Segment for each continuous span of track data. - - -* Class name: Segment -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md), [phpGPX\Models\StatsCalculator](phpGPX-Models-StatsCalculator.md) - - - - -Properties ----------- - - -### $points - - public array $points - -Array of segment points - - - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - -You can add extend GPX by adding your own elements from another schema here. - - - -* Visibility: **public** - - -### $stats - - public \phpGPX\Models\Stats $stats - - - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Segment::__construct() - -Segment constructor. - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -### recalculateStats - - void phpGPX\Models\StatsCalculator::recalculateStats() - -Recalculate stats objects. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\StatsCalculator](phpGPX-Models-StatsCalculator.md) - - - diff --git a/docs/phpGPX-Models-Stats.md b/docs/phpGPX-Models-Stats.md deleted file mode 100644 index a133f1f..0000000 --- a/docs/phpGPX-Models-Stats.md +++ /dev/null @@ -1,188 +0,0 @@ -phpGPX\Models\Stats -=============== - -Class Stats - - - - -* Class name: Stats -* Namespace: phpGPX\Models -* This class implements: [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -Properties ----------- - - -### $distance - - public float $distance - -Distance in meters (m) - - - -* Visibility: **public** - - -### $averageSpeed - - public float $averageSpeed = null - -Average speed in meters per second (m/s) - - - -* Visibility: **public** - - -### $averagePace - - public float $averagePace = null - -Average pace in seconds per kilometer (s/km) - - - -* Visibility: **public** - - -### $minAltitude - - public integer $minAltitude = null - -Minimal altitude in meters (m) - - - -* Visibility: **public** - -### $minAltitudeCoords - - public [float,float] $minAltitudeCoords = null - -Minimal altitude coordinates in associative array with keys: "lat" for latitude & "lng" for longitude - - - -* Visibility: **public** - - -### $maxAltitude - - public integer $maxAltitude = null - -Maximal altitude in meters (m) - - - -* Visibility: **public** - -### $maxAltitudeCoords - - public [float,float] $maxAltitudeCoords = null - -Maximal altitude coordinates in associative array with keys: "lat" for latitude & "lng" for longitude - - - -* Visibility: **public** - - -### $cumulativeElevationGain - - public integer $cumulativeElevationGain = null - -Cumulative elevation gain in meters (m) - - - -* Visibility: **public** - - -### $startedAt - - public \DateTime $startedAt = null - -Started time - - - -* Visibility: **public** - -### $startedAtCoords - - public [float,float] $startedAtCoords = null - -Started coordinates in associative array with keys: "lat" for latitude & "lng" for longitude - - - -* Visibility: **public** - - -### $finishedAt - - public \DateTime $finishedAt = null - -Ending time - - - -* Visibility: **public** - -### $finishedAtCoords - - public [float,float] $finishedAtCoords = null - -Ending coordinates in associative array with keys: "lat" for latitude & "lng" for longitude - - - -* Visibility: **public** - - -### $duration - - public integer $duration = null - -Duration is seconds - - - -* Visibility: **public** - - -Methods -------- - - -### reset - - mixed phpGPX\Models\Stats::reset() - -Reset all stats - - - -* Visibility: **public** - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - diff --git a/docs/phpGPX-Models-StatsCalculator.md b/docs/phpGPX-Models-StatsCalculator.md deleted file mode 100644 index 0082da1..0000000 --- a/docs/phpGPX-Models-StatsCalculator.md +++ /dev/null @@ -1,33 +0,0 @@ -phpGPX\Models\StatsCalculator -=============== - - - - - - -* Interface name: StatsCalculator -* Namespace: phpGPX\Models -* This is an **interface** - - - - - - -Methods -------- - - -### recalculateStats - - void phpGPX\Models\StatsCalculator::recalculateStats() - -Recalculate stats objects. - - - -* Visibility: **public** - - - diff --git a/docs/phpGPX-Models-Summarizable.md b/docs/phpGPX-Models-Summarizable.md deleted file mode 100644 index cee6fe6..0000000 --- a/docs/phpGPX-Models-Summarizable.md +++ /dev/null @@ -1,33 +0,0 @@ -phpGPX\Models\Summarizable -=============== - -Interface Summarizable - - - - -* Interface name: Summarizable -* Namespace: phpGPX\Models -* This is an **interface** - - - - - - -Methods -------- - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** - - - diff --git a/docs/phpGPX-Models-Track.md b/docs/phpGPX-Models-Track.md deleted file mode 100644 index 84b3d5b..0000000 --- a/docs/phpGPX-Models-Track.md +++ /dev/null @@ -1,190 +0,0 @@ -phpGPX\Models\Track -=============== - -Class Track - - - - -* Class name: Track -* Namespace: phpGPX\Models -* Parent class: [phpGPX\Models\Collection](phpGPX-Models-Collection.md) - - - - - -Properties ----------- - - -### $segments - - public array $segments - -Array of Track segments - - - -* Visibility: **public** - - -### $name - - public string $name - -GPS name of route / track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $comment - - public string $comment - -GPS comment for route. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $description - - public string $description - -Text description of route/track for user. Not sent to GPS. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $source - - public string $source - -Source of data. Included to give user some idea of reliability and accuracy of data. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $links - - public array $links - -Links to external information about the route/track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $number - - public integer $number - -GPS route/track number. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $type - - public string $type - -Type (classification) of route/track. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $extensions - - public \phpGPX\Models\Extensions $extensions - -You can add extend GPX by adding your own elements from another schema here. - -An original GPX 1.1 attribute. - -* Visibility: **public** - - -### $stats - - public \phpGPX\Models\Stats $stats - -Objects contains calculated statistics for collection. - - - -* Visibility: **public** - - -Methods -------- - - -### __construct - - mixed phpGPX\Models\Collection::__construct() - -Collection constructor. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Collection](phpGPX-Models-Collection.md) - - - - -### getPoints - - array phpGPX\Models\Collection::getPoints() - -Return all points in collection. - - - -* Visibility: **public** -* This method is **abstract**. -* This method is defined by [phpGPX\Models\Collection](phpGPX-Models-Collection.md) - - - - -### toArray - - array phpGPX\Models\Summarizable::toArray() - -Serialize object to array - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\Summarizable](phpGPX-Models-Summarizable.md) - - - - -### recalculateStats - - void phpGPX\Models\StatsCalculator::recalculateStats() - -Recalculate stats objects. - - - -* Visibility: **public** -* This method is defined by [phpGPX\Models\StatsCalculator](phpGPX-Models-StatsCalculator.md) - - - diff --git a/docs/phpGPX-Parsers-BoundsParser.md b/docs/phpGPX-Parsers-BoundsParser.md deleted file mode 100644 index e419656..0000000 --- a/docs/phpGPX-Parsers-BoundsParser.md +++ /dev/null @@ -1,70 +0,0 @@ -phpGPX\Parsers\BoundsParser -=============== - -Class BoundsParser - - - - -* Class name: BoundsParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - private mixed $tagName = 'bounds' - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Bounds|null phpGPX\Parsers\BoundsParser::parse(\SimpleXMLElement $node) - -Parse data from XML. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - \DOMElement phpGPX\Parsers\BoundsParser::toXML(\phpGPX\Models\Bounds $bounds, \DOMDocument $document) - -Create XML representation. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $bounds **[phpGPX\Models\Bounds](phpGPX-Models-Bounds.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-CopyrightParser.md b/docs/phpGPX-Parsers-CopyrightParser.md deleted file mode 100644 index 9804ee0..0000000 --- a/docs/phpGPX-Parsers-CopyrightParser.md +++ /dev/null @@ -1,70 +0,0 @@ -phpGPX\Parsers\CopyrightParser -=============== - -Class CopyrightParser - - - - -* Class name: CopyrightParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - public mixed $tagName = 'copyright' - - - - - -* Visibility: **public** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Copyright|null phpGPX\Parsers\CopyrightParser::parse(\SimpleXMLElement $node) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - \DOMElement phpGPX\Parsers\CopyrightParser::toXML(\phpGPX\Models\Copyright $copyright, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $copyright **[phpGPX\Models\Copyright](phpGPX-Models-Copyright.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-EmailParser.md b/docs/phpGPX-Parsers-EmailParser.md deleted file mode 100644 index b746840..0000000 --- a/docs/phpGPX-Parsers-EmailParser.md +++ /dev/null @@ -1,70 +0,0 @@ -phpGPX\Parsers\EmailParser -=============== - -Class EmailParser - - - - -* Class name: EmailParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - private mixed $tagName = 'email' - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Email phpGPX\Parsers\EmailParser::parse(\SimpleXMLElement $node) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - \DOMElement phpGPX\Parsers\EmailParser::toXML(\phpGPX\Models\Email $email, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $email **[phpGPX\Models\Email](phpGPX-Models-Email.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-ExtensionParser.md b/docs/phpGPX-Parsers-ExtensionParser.md deleted file mode 100644 index 3de29a1..0000000 --- a/docs/phpGPX-Parsers-ExtensionParser.md +++ /dev/null @@ -1,82 +0,0 @@ -phpGPX\Parsers\ExtensionParser -=============== - -Class ExtensionParser - - - - -* Class name: ExtensionParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - public mixed $tagName = 'extensions' - - - - - -* Visibility: **public** -* This property is **static**. - - -### $usedNamespaces - - public mixed $usedNamespaces = array() - - - - - -* Visibility: **public** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Extensions phpGPX\Parsers\ExtensionParser::parse(\SimpleXMLElement $nodes) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $nodes **SimpleXMLElement** - - - -### toXML - - \DOMElement|null phpGPX\Parsers\ExtensionParser::toXML(\phpGPX\Models\Extensions $extensions, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $extensions **[phpGPX\Models\Extensions](phpGPX-Models-Extensions.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-Extensions-TrackPointExtensionParser.md b/docs/phpGPX-Parsers-Extensions-TrackPointExtensionParser.md deleted file mode 100644 index 987d5e3..0000000 --- a/docs/phpGPX-Parsers-Extensions-TrackPointExtensionParser.md +++ /dev/null @@ -1,69 +0,0 @@ -phpGPX\Parsers\Extensions\TrackPointExtensionParser -=============== - - - - - - -* Class name: TrackPointExtensionParser -* Namespace: phpGPX\Parsers\Extensions - - - - - -Properties ----------- - - -### $attributeMapper - - private mixed $attributeMapper = array('atemp' => array('name' => 'aTemp', 'type' => 'float'), 'wtemp' => array('name' => 'wTemp', 'type' => 'float'), 'depth' => array('name' => 'depth', 'type' => 'float'), 'hr' => array('name' => 'hr', 'type' => 'float'), 'cad' => array('name' => 'cad', 'type' => 'float'), 'speed' => array('name' => 'speed', 'type' => 'float'), 'course' => array('name' => 'course', 'type' => 'int'), 'bearing' => array('name' => 'bearing', 'type' => 'int')) - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Extensions\TrackPointExtension phpGPX\Parsers\Extensions\TrackPointExtensionParser::parse(\SimpleXMLElement $node) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - \DOMElement phpGPX\Parsers\Extensions\TrackPointExtensionParser::toXML(\phpGPX\Models\Extensions\TrackPointExtension $extension, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $extension **[phpGPX\Models\Extensions\TrackPointExtension](phpGPX-Models-Extensions-TrackPointExtension.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-LinkParser.md b/docs/phpGPX-Parsers-LinkParser.md deleted file mode 100644 index 983e8c6..0000000 --- a/docs/phpGPX-Parsers-LinkParser.md +++ /dev/null @@ -1,88 +0,0 @@ -phpGPX\Parsers\LinkParser -=============== - - - - - - -* Class name: LinkParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - private mixed $tagName = 'link' - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - array phpGPX\Parsers\LinkParser::parse(array $nodes) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $nodes **array<mixed,\SimpleXMLElement>** - - - -### toXMLArray - - array phpGPX\Parsers\LinkParser::toXMLArray(array $links, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $links **array<mixed,\phpGPX\Models\Link>** -* $document **DOMDocument** - - - -### toXML - - \DOMElement phpGPX\Parsers\LinkParser::toXML(\phpGPX\Models\Link $link, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $link **[phpGPX\Models\Link](phpGPX-Models-Link.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-MetadataParser.md b/docs/phpGPX-Parsers-MetadataParser.md deleted file mode 100644 index a7b1e13..0000000 --- a/docs/phpGPX-Parsers-MetadataParser.md +++ /dev/null @@ -1,82 +0,0 @@ -phpGPX\Parsers\MetadataParser -=============== - -Class MetadataParser - - - - -* Class name: MetadataParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - private mixed $tagName = 'metadata' - - - - - -* Visibility: **private** -* This property is **static**. - - -### $attributeMapper - - private mixed $attributeMapper = array('name' => array('name' => 'name', 'type' => 'string'), 'desc' => array('name' => 'description', 'type' => 'string'), 'author' => array('name' => 'author', 'type' => 'object'), 'copyright' => array('name' => 'copyright', 'type' => 'object'), 'link' => array('name' => 'links', 'type' => 'array'), 'time' => array('name' => 'time', 'type' => 'object'), 'keywords' => array('name' => 'keywords', 'type' => 'string'), 'bounds' => array('name' => 'bounds', 'type' => 'object'), 'extensions' => array('name' => 'extensions', 'type' => 'object')) - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Metadata phpGPX\Parsers\MetadataParser::parse(\SimpleXMLElement $node) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - mixed phpGPX\Parsers\MetadataParser::toXML(\phpGPX\Models\Metadata $metadata, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $metadata **[phpGPX\Models\Metadata](phpGPX-Models-Metadata.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-PersonParser.md b/docs/phpGPX-Parsers-PersonParser.md deleted file mode 100644 index 58fff01..0000000 --- a/docs/phpGPX-Parsers-PersonParser.md +++ /dev/null @@ -1,70 +0,0 @@ -phpGPX\Parsers\PersonParser -=============== - -Class PersonParser - - - - -* Class name: PersonParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - public mixed $tagName = 'author' - - - - - -* Visibility: **public** -* This property is **static**. - - -Methods -------- - - -### parse - - \phpGPX\Models\Person phpGPX\Parsers\PersonParser::parse(\SimpleXMLElement $node) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - mixed phpGPX\Parsers\PersonParser::toXML(\phpGPX\Models\Person $person, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $person **[phpGPX\Models\Person](phpGPX-Models-Person.md)** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-PointParser.md b/docs/phpGPX-Parsers-PointParser.md deleted file mode 100644 index c91db17..0000000 --- a/docs/phpGPX-Parsers-PointParser.md +++ /dev/null @@ -1,100 +0,0 @@ -phpGPX\Parsers\PointParser -=============== - - - - - - -* Class name: PointParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $attributeMapper - - private mixed $attributeMapper = array('ele' => array('name' => 'elevation', 'type' => 'float'), 'time' => array('name' => 'time', 'type' => 'object'), 'magvar' => array('name' => 'magVar', 'type' => 'float'), 'geoidheight' => array('name' => 'geoidHeight', 'type' => 'float'), 'name' => array('name' => 'name', 'type' => 'string'), 'cmt' => array('name' => 'comment', 'type' => 'string'), 'desc' => array('name' => 'description', 'type' => 'string'), 'src' => array('name' => 'source', 'type' => 'string'), 'link' => array('name' => 'links', 'type' => 'object'), 'sym' => array('name' => 'symbol', 'type' => 'string'), 'type' => array('name' => 'type', 'type' => 'string'), 'fix' => array('name' => 'fix', 'type' => 'string'), 'sat' => array('name' => 'satellitesNumber', 'type' => 'integer'), 'hdop' => array('name' => 'hdop', 'type' => 'float'), 'vdop' => array('name' => 'vdop', 'type' => 'float'), 'pdop' => array('name' => 'pdop', 'type' => 'float'), 'ageofdgpsdata' => array('name' => 'ageOfGpsData', 'type' => 'float'), 'dgpsid' => array('name' => 'dgpsid', 'type' => 'integer'), 'extensions' => array('name' => 'extensions', 'type' => 'object')) - - - - - -* Visibility: **private** -* This property is **static**. - - -### $typeMapper - - private mixed $typeMapper = array('trkpt' => \phpGPX\Models\Point::TRACKPOINT, 'wpt' => \phpGPX\Models\Point::WAYPOINT, 'rtp' => \phpGPX\Models\Point::ROUTEPOINT) - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - mixed phpGPX\Parsers\PointParser::parse(\SimpleXMLElement $node) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $node **SimpleXMLElement** - - - -### toXML - - \DOMElement phpGPX\Parsers\PointParser::toXML(\phpGPX\Models\Point $point, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $point **[phpGPX\Models\Point](phpGPX-Models-Point.md)** -* $document **DOMDocument** - - - -### toXMLArray - - array phpGPX\Parsers\PointParser::toXMLArray(array $points, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $points **array** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-RouteParser.md b/docs/phpGPX-Parsers-RouteParser.md deleted file mode 100644 index 38d4240..0000000 --- a/docs/phpGPX-Parsers-RouteParser.md +++ /dev/null @@ -1,100 +0,0 @@ -phpGPX\Parsers\RouteParser -=============== - -Class RouteParser - - - - -* Class name: RouteParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - public mixed $tagName = 'rte' - - - - - -* Visibility: **public** -* This property is **static**. - - -### $attributeMapper - - private mixed $attributeMapper = array('name' => array('name' => 'name', 'type' => 'string'), 'cmt' => array('name' => 'comment', 'type' => 'string'), 'desc' => array('name' => 'description', 'type' => 'string'), 'src' => array('name' => 'source', 'type' => 'string'), 'link' => array('name' => 'links', 'type' => 'array'), 'number' => array('name' => 'number', 'type' => 'integer'), 'type' => array('name' => 'type', 'type' => 'string'), 'extensions' => array('name' => 'extensions', 'type' => 'object'), 'rtep' => array('name' => 'points', 'type' => 'array')) - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - array phpGPX\Parsers\RouteParser::parse(array $nodes) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $nodes **array<mixed,\SimpleXMLElement>** - - - -### toXML - - \DOMElement phpGPX\Parsers\RouteParser::toXML(\phpGPX\Models\Route $route, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $route **[phpGPX\Models\Route](phpGPX-Models-Route.md)** -* $document **DOMDocument** - - - -### toXMLArray - - array phpGPX\Parsers\RouteParser::toXMLArray(array $routes, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $routes **array** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-SegmentParser.md b/docs/phpGPX-Parsers-SegmentParser.md deleted file mode 100644 index bd4ab3b..0000000 --- a/docs/phpGPX-Parsers-SegmentParser.md +++ /dev/null @@ -1,88 +0,0 @@ -phpGPX\Parsers\SegmentParser -=============== - -Class SegmentParser - - - - -* Class name: SegmentParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - public mixed $tagName = 'trkseg' - - - - - -* Visibility: **public** -* This property is **static**. - - -Methods -------- - - -### parse - - array phpGPX\Parsers\SegmentParser::parse($nodes) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $nodes **mixed** - <p>\SimpleXMLElement[]</p> - - - -### toXML - - \DOMElement phpGPX\Parsers\SegmentParser::toXML(\phpGPX\Models\Segment $segment, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $segment **[phpGPX\Models\Segment](phpGPX-Models-Segment.md)** -* $document **DOMDocument** - - - -### toXMLArray - - array phpGPX\Parsers\SegmentParser::toXMLArray(array $segments, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $segments **array** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-TrackParser.md b/docs/phpGPX-Parsers-TrackParser.md deleted file mode 100644 index 3de14d5..0000000 --- a/docs/phpGPX-Parsers-TrackParser.md +++ /dev/null @@ -1,100 +0,0 @@ -phpGPX\Parsers\TrackParser -=============== - -Class TrackParser - - - - -* Class name: TrackParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - -Properties ----------- - - -### $tagName - - public mixed $tagName = 'trk' - - - - - -* Visibility: **public** -* This property is **static**. - - -### $attributeMapper - - private mixed $attributeMapper = array('name' => array('name' => 'name', 'type' => 'string'), 'cmt' => array('name' => 'comment', 'type' => 'string'), 'desc' => array('name' => 'description', 'type' => 'string'), 'src' => array('name' => 'source', 'type' => 'string'), 'link' => array('name' => 'links', 'type' => 'array'), 'number' => array('name' => 'number', 'type' => 'integer'), 'type' => array('name' => 'type', 'type' => 'string'), 'extensions' => array('name' => 'extensions', 'type' => 'object'), 'trkseg' => array('name' => 'segments', 'type' => 'array')) - - - - - -* Visibility: **private** -* This property is **static**. - - -Methods -------- - - -### parse - - array phpGPX\Parsers\TrackParser::parse(\SimpleXMLElement $nodes) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $nodes **SimpleXMLElement** - - - -### toXML - - \DOMElement phpGPX\Parsers\TrackParser::toXML(\phpGPX\Models\Track $track, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $track **[phpGPX\Models\Track](phpGPX-Models-Track.md)** -* $document **DOMDocument** - - - -### toXMLArray - - array phpGPX\Parsers\TrackParser::toXMLArray(array $tracks, \DOMDocument $document) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $tracks **array** -* $document **DOMDocument** - - diff --git a/docs/phpGPX-Parsers-WaypointParser.md b/docs/phpGPX-Parsers-WaypointParser.md deleted file mode 100644 index b1b4811..0000000 --- a/docs/phpGPX-Parsers-WaypointParser.md +++ /dev/null @@ -1,40 +0,0 @@ -phpGPX\Parsers\WaypointParser -=============== - -Class WaypointParser - - - - -* Class name: WaypointParser -* Namespace: phpGPX\Parsers -* This is an **abstract** class - - - - - - - -Methods -------- - - -### parse - - array phpGPX\Parsers\WaypointParser::parse(\SimpleXMLElement $nodes) - - - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $nodes **SimpleXMLElement** - <ul> -<li>a non empty list of wpt elements</li> -</ul> - - diff --git a/docs/phpGPX-phpGPX.md b/docs/phpGPX-phpGPX.md deleted file mode 100644 index f21b141..0000000 --- a/docs/phpGPX-phpGPX.md +++ /dev/null @@ -1,164 +0,0 @@ -phpGPX\phpGPX -=============== - -Class phpGPX - - - - -* Class name: phpGPX -* Namespace: phpGPX - - - -Constants ----------- - - -### JSON_FORMAT - - const JSON_FORMAT = 'json' - - - - - -### XML_FORMAT - - const XML_FORMAT = 'xml' - - - - - -### PACKAGE_NAME - - const PACKAGE_NAME = 'phpGPX' - - - - - -### VERSION - - const VERSION = '1.0' - - - - - -Properties ----------- - - -### $CALCULATE_STATS - - public boolean $CALCULATE_STATS = true - -Create Stats object for each track, segment and route - - - -* Visibility: **public** -* This property is **static**. - - -### $SORT_BY_TIMESTAMP - - public boolean $SORT_BY_TIMESTAMP = false - -Additional sort based on timestamp in Routes & Tracks on XML read. - -Disabled by default, data should be already sorted. - -* Visibility: **public** -* This property is **static**. - - -### $DATETIME_FORMAT - - public string $DATETIME_FORMAT = 'c' - -Default DateTime output format in JSON serialization. - - - -* Visibility: **public** -* This property is **static**. - - -### $DATETIME_TIMEZONE_OUTPUT - - public string $DATETIME_TIMEZONE_OUTPUT = 'UTC' - -Default timezone for display. - -Data are always stored in UTC timezone. - -* Visibility: **public** -* This property is **static**. - - -### $PRETTY_PRINT - - public boolean $PRETTY_PRINT = true - -Pretty print. - - - -* Visibility: **public** -* This property is **static**. - - -Methods -------- - - -### load - - \phpGPX\Models\GpxFile phpGPX\phpGPX::load($path) - -Load GPX file. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $path **mixed** - - - -### parse - - \phpGPX\Models\GpxFile phpGPX\phpGPX::parse($xml) - -Parse GPX data string. - - - -* Visibility: **public** -* This method is **static**. - - -#### Arguments -* $xml **mixed** - - - -### getSignature - - string phpGPX\phpGPX::getSignature() - -Create library signature from name and version. - - - -* Visibility: **public** -* This method is **static**. - - - diff --git a/docs/schemas/GpxExtensionsv3.xsd b/docs/schemas/GpxExtensionsv3.xsd new file mode 100644 index 0000000..5184ba0 --- /dev/null +++ b/docs/schemas/GpxExtensionsv3.xsd @@ -0,0 +1,215 @@ + + + + + This schema defines the Garmin extensions to be used with the GPX 1.1 schema. + The root elements defined by this schema are intended to be used as child + elements of the "extensions" elements in the GPX 1.1 schema. The GPX 1.1 + schema is available at http://www.topografix.com/GPX/1/1/gpx.xsd. + + + + + + + + + + + + + + + This type contains data fields available in Garmin GDB waypoints that cannot + be represented in waypoints in GPX 1.1 instances. + + + + + + + + + + + + + + + + This type contains a list of categories to which a waypoint has been assigned. + Note that this list may contain categories which do not exist for a particular + application installation. + + + + + + + + + + + + + + + + + + + + + + + Category provides the ability to specify the type of a + phone number. For example, a phone number can be categorized as + "Home", "Work", "Mobile" e.t.c + + + + + + + + + This type contains data fields available in Garmin GDB routes that cannot + be represented in routes in GPX 1.1 instances. + + + + + + + + + + + This type contains data fields available in Garmin GDB routes that cannot + be represented in routes in GPX 1.1 instances. + + + + + + + + + + + This type contains data fields available in Garmin GDB tracks that cannot + be represented in routes in GPX 1.1 instances. + + + + + + + + + + This type contains data fields available in Garmin GDB track points that cannot + be represented in track points in GPX 1.1 instances. + + + + + + + + + + + This type contains a temperature value measured in degrees Celsius. + + + + + + + This type contains a distance value measured in meters. + + + + + + + This type contains a string that specifies how a waypoint should be + displayed on a map. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + + The longitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + + This type provides the ability to extend any data type that includes it. + + + + + + + diff --git a/docs/schemas/TrackPointExtensionv1.xsd b/docs/schemas/TrackPointExtensionv1.xsd new file mode 100644 index 0000000..89ff220 --- /dev/null +++ b/docs/schemas/TrackPointExtensionv1.xsd @@ -0,0 +1,74 @@ + + + + + This schema defines Garmin extensions to be used with the GPX 1.1 schema. + The root element defined by this schema is intended to be used as a child + element of the "extensions" elements in the trkpt element in the GPX 1.1 schema. + The GPX 1.1 schema is available at http://www.topografix.com/GPX/1/1/gpx.xsd. + This is a replacement for TrackPointExtension in + http://www.garmin.com/xmlschemas/GpxExtensions/v3 + + + + + + + This type contains data fields that cannot + be represented in track points in GPX 1.1 instances. + + + + + + + + + + + + + + This type contains a temperature value measured in degrees Celsius. + + + + + + + This type contains a distance value measured in meters. + + + + + + + This type contains a heart rate measured in beats per minute. + + + + + + + + + This type contains a cadence measured in revolutions per minute. + + + + + + + + + This type provides the ability to extend any data type that includes it. + + + + + + + diff --git a/docs/schemas/gpx.xsd b/docs/schemas/gpx.xsd new file mode 100644 index 0000000..d9441f6 --- /dev/null +++ b/docs/schemas/gpx.xsd @@ -0,0 +1,788 @@ + + + + + + GPX schema version 1.1 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp + + GPX uses the following conventions: all coordinates are relative to the WGS84 datum. All measurements are in metric units. + + + + + + + GPX is the root element in the XML file. + + + + + + + + GPX documents contain a metadata header, followed by waypoints, routes, and tracks. You can add your own elements + to the extensions section of the GPX document. + + + + + + + Metadata about the file. + + + + + + + A list of waypoints. + + + + + + + A list of routes. + + + + + + + A list of tracks. + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + You must include the version number in your GPX document. + + + + + + + You must include the name or URL of the software that created your GPX document. This allows others to + inform the creator of a GPX instance document that fails to validate. + + + + + + + + + Information about the GPX file, author, and copyright restrictions goes in the metadata section. Providing rich, + meaningful information about your GPX files allows others to search for and use your GPS data. + + + + + + + The name of the GPX file. + + + + + + + A description of the contents of the GPX file. + + + + + + + The person or organization who created the GPX file. + + + + + + + Copyright and license information governing use of the file. + + + + + + + URLs associated with the location described in the file. + + + + + + + The creation date of the file. + + + + + + + Keywords associated with the file. Search engines or databases can use this information to classify the data. + + + + + + + Minimum and maximum coordinates which describe the extent of the coordinates in the file. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + wpt represents a waypoint, point of interest, or named feature on a map. + + + + + + + + Elevation (in meters) of the point. + + + + + + + Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! Conforms to ISO 8601 specification for date/time representation. Fractional seconds are allowed for millisecond timing in tracklogs. + + + + + + + Magnetic variation (in degrees) at the point + + + + + + + Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. + + + + + + + + + The GPS name of the waypoint. This field will be transferred to and from the GPS. GPX does not place restrictions on the length of this field or the characters contained in it. It is up to the receiving application to validate the field before sending it to the GPS. + + + + + + + GPS waypoint comment. Sent to GPS as comment. + + + + + + + A text description of the element. Holds additional information about the element intended for the user, not the GPS. + + + + + + + Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. + + + + + + + Link to additional information about the waypoint. + + + + + + + Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. If the GPS abbreviates words, spell them out. + + + + + + + Type (classification) of the waypoint. + + + + + + + + + Type of GPX fix. + + + + + + + Number of satellites used to calculate the GPX fix. + + + + + + + Horizontal dilution of precision. + + + + + + + Vertical dilution of precision. + + + + + + + Position dilution of precision. + + + + + + + Number of seconds since last DGPS update. + + + + + + + ID of DGPS station used in differential correction. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + The latitude of the point. This is always in decimal degrees, and always in WGS84 datum. + + + + + + + The longitude of the point. This is always in decimal degrees, and always in WGS84 datum. + + + + + + + + + rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination. + + + + + + + GPS name of route. + + + + + + + GPS comment for route. + + + + + + + Text description of route for user. Not sent to GPS. + + + + + + + Source of data. Included to give user some idea of reliability and accuracy of data. + + + + + + + Links to external information about the route. + + + + + + + GPS route number. + + + + + + + Type (classification) of route. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + A list of route points. + + + + + + + + + + trk represents a track - an ordered list of points describing a path. + + + + + + + GPS name of track. + + + + + + + GPS comment for track. + + + + + + + User description of track. + + + + + + + Source of data. Included to give user some idea of reliability and accuracy of data. + + + + + + + Links to external information about track. + + + + + + + GPS track number. + + + + + + + Type (classification) of track. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. + + + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. + + + + + + + A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + Information about the copyright holder and any license governing use of this file. By linking to an appropriate license, + you may place your data into the public domain or grant additional usage rights. + + + + + + + Year of copyright. + + + + + + + Link to external file containing license text. + + + + + + + + Copyright holder (TopoSoft, Inc.) + + + + + + + + + A link to an external resource (Web page, digital photo, video clip, etc) with additional information. + + + + + + + Text of hyperlink. + + + + + + + Mime type of content (image/jpeg) + + + + + + + + URL of hyperlink. + + + + + + + + + An email address. Broken into two parts (id and domain) to help prevent email harvesting. + + + + + + id half of email address (billgates2004) + + + + + + + domain half of email address (hotmail.com) + + + + + + + + + A person or organization. + + + + + + + Name of person or organization. + + + + + + + Email address. + + + + + + + Link to Web site or other external information about person. + + + + + + + + + + A geographic point with optional elevation and time. Available for use by other schemas. + + + + + + + The elevation (in meters) of the point. + + + + + + + The time that the point was recorded. + + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + An ordered sequence of points. (for polygons or polylines, e.g.) + + + + + + + Ordered list of geographic points. + + + + + + + + + + Two lat/lon pairs defining the extent of an element. + + + + + + The minimum latitude. + + + + + + + The minimum longitude. + + + + + + + The maximum latitude. + + + + + + + The maximum longitude. + + + + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + + + + The longitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + + + + Used for bearing, heading, course. Units are decimal degrees, true (not magnetic). + + + + + + + + + + + + Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used + + + + + + + + + + + + + + + Represents a differential GPS station. + + + + + + + + + diff --git a/docs/schemas/gpx_style2.xsd b/docs/schemas/gpx_style2.xsd new file mode 100644 index 0000000..b48c9a6 --- /dev/null +++ b/docs/schemas/gpx_style2.xsd @@ -0,0 +1,428 @@ + + + + + + + + Graphical style for a linear feature (route, track, border of filled object, etc). + + + + + + + Background fill pattern for a filled object (closed track, polygon, text box, etc) + + + + + + + Text size, color, style + + + + + + + Generic horizontal alignment (for text, e.g.) + + + + + + + Generic vertical alignment (for text, e.g.) + + + + + + + + + + Graphical style for a linear feature (route, track, border of filled object, etc). + + + + + + + Line color + + + + + + + Line opacity (0.0 = transparent; 1.0 = fully opaque) + + + + + + + Width, in millimeters, of the line + + + + + + + Dash, e.g. + + + + + + + butt, e.g. + + + + + + + List of marks and spaces which define the dash pattern. Units are millimeters. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + + Background fill style for filled object (closed track, polygon, text box, etc) + + + + + + + Fill color + + + + + + + Fill opacity (0.0 = fully transparent; 1.0 = fully opaque) + + + + + + + Diagonal, e.g. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + + Text font, size, color, etc. + + + + + + + Text color + + + + + + + Text opacity (0.0 = fully transparent; 1.0 = fully opaque) + + + + + + + Text font + + + + + + + Text alignment (left, right, center) + + + + + + + Vertical text alignment (top, center, bottom) + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + + Font size and style + + + + + + + Font face and generic family name + + + + + + + Font size in millimeters (not pixels!) + + + + + + + bold, e.g. + + + + + + + italic, e.g. + + + + + + + uppercase, e.g. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + + if you specify a font, you must specify a generic "fallback" + + + + + + + Font face: arial, e.g. + + + + + + + + sans-serif, e.g. + + + + + + + + + + Dasharray contains one or more dash elements + + + + + + + Each dash element defines a mark and a space on the line + + + + + + + + + + mark and space of a dash pattern. Units are millimeters + + + + + + mark length in millimeters + + + + + + + space length in millimeters + + + + + + + + + + see http://www.w3.org/TR/REC-CSS2/fonts.html#generic-font-families + + + + + + + + + + + + + + + http://www.w3.org/TR/REC-CSS2/text.html#caps-prop + + + + + + + + + + + + + + Hex RGB color (FF0000 = red) + + + + + + + + + + + 0.0 = completely transparent; 1.0 = completely opaque + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + \ No newline at end of file diff --git a/example/CreateFileFromScratch.php b/examples/CreateFileFromScratch.php similarity index 100% rename from example/CreateFileFromScratch.php rename to examples/CreateFileFromScratch.php diff --git a/example/Example.php b/examples/Example.php similarity index 100% rename from example/Example.php rename to examples/Example.php diff --git a/example/endomondo.gpx b/examples/endomondo.gpx similarity index 100% rename from example/endomondo.gpx rename to examples/endomondo.gpx diff --git a/example/output_waypoint_test.gpx b/examples/output_waypoint_test.gpx similarity index 100% rename from example/output_waypoint_test.gpx rename to examples/output_waypoint_test.gpx diff --git a/example/waypoint_test.gpx b/examples/waypoint_test.gpx similarity index 100% rename from example/waypoint_test.gpx rename to examples/waypoint_test.gpx diff --git a/example/waypoints_create.php b/examples/waypoints_create.php similarity index 100% rename from example/waypoints_create.php rename to examples/waypoints_create.php diff --git a/example/waypoints_load.php b/examples/waypoints_load.php similarity index 100% rename from example/waypoints_load.php rename to examples/waypoints_load.php diff --git a/phpunit.xml b/phpunit.xml index 8065e11..1ef0299 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,26 +1,26 @@ - - - src/phpGPX/Tests - - + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" + bootstrap="vendor/autoload.php" + cacheDirectory=".phpunit.cache" + executionOrder="depends,defects" + requireCoverageMetadata="true" + beStrictAboutCoverageMetadata="true" + beStrictAboutOutputDuringTests="true" + failOnRisky="true" + failOnWarning="true"> + + + tests/phpGPX + + - - - src - - - src/phpGPX/Tests - - + + + src + + + tests/phpGPX + + diff --git a/src/phpGPX/Config.php b/src/phpGPX/Config.php new file mode 100644 index 0000000..4e001c8 --- /dev/null +++ b/src/phpGPX/Config.php @@ -0,0 +1,82 @@ +setTimezone(new \DateTimeZone($timezone)); - $formatted = $datetime->format($format); + $formatted = $datetime->format($format); } return $formatted; diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php index f0fd238..b697c71 100644 --- a/src/phpGPX/Models/Bounds.php +++ b/src/phpGPX/Models/Bounds.php @@ -6,40 +6,43 @@ namespace phpGPX\Models; -class Bounds implements Summarizable +use phpGPX\GpxSerializable; + +class Bounds implements \JsonSerializable, GpxSerializable { + public const TAG_NAME = 'bounds'; /** * Minimal latitude in file. - * @var float + * @var float|null */ - public float $minLatitude; + public ?float $minLatitude; /** * Minimal longitude in file. - * @var float + * @var float|null */ - public float $minLongitude; + public ?float $minLongitude; /** * Maximal latitude in file. - * @var float + * @var float|null */ - public float $maxLatitude; + public ?float $maxLatitude; /** * Maximal longitude in file. - * @var float + * @var float|null */ - public float $maxLongitude; + public ?float $maxLongitude; /** - * @param float $minLatitude - * @param float $minLongitude - * @param float $maxLatitude - * @param float $maxLongitude + * @param float|null $minLatitude + * @param float|null $minLongitude + * @param float|null $maxLatitude + * @param float|null $maxLongitude */ - public function __construct(float $minLatitude, float $minLongitude, float $maxLatitude, float $maxLongitude) + public function __construct(?float $minLatitude, ?float $minLongitude, ?float $maxLatitude, ?float $maxLongitude) { $this->minLatitude = $minLatitude; $this->minLongitude = $minLongitude; @@ -47,18 +50,26 @@ public function __construct(float $minLatitude, float $minLongitude, float $maxL $this->maxLongitude = $maxLongitude; } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array + * GeoJSON serializer + * @return array + */ + public function jsonSerialize(): array { - return [ - 'minlat' => $this->minLatitude, - 'minlon' => $this->minLongitude, - 'maxlat' => $this->maxLatitude, - 'maxlon' => $this->maxLongitude - ]; - } + return [$this->minLongitude, $this->minLatitude, $this->maxLongitude, $this->maxLatitude]; + } + + public static function parse(\SimpleXMLElement $node): ?Bounds + { + if ($node->getName() != self::TAG_NAME) { + return null; + } + + return new Bounds( + (float) $node['minlat'], + (float) $node['minlon'], + (float) $node['maxlat'], + (float) $node['maxlon'] + ); + } } diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index a8409e2..d18c984 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -10,6 +10,13 @@ use phpGPX\Helpers\DateTimeHelper; use phpGPX\phpGPX; +enum PointType: string +{ + case waypoint = 'wpt'; + case trackpoint = 'trkpt'; + case routepoint = 'rtept'; +} + /** * Class Point * GPX point representation according to GPX 1.1 specification. diff --git a/src/phpGPX/Tests/CreateWaypointTest.php b/src/phpGPX/Tests/CreateWaypointTest.php deleted file mode 100644 index acec001..0000000 --- a/src/phpGPX/Tests/CreateWaypointTest.php +++ /dev/null @@ -1,128 +0,0 @@ - 9.860624216140083, - 'latitude' => 54.9328621088893, - 'elevation' => 0, - 'time' => new \DateTime("+ 1 MINUTE") - ], - [ - 'latitude' => 54.83293237320851, - 'longitude' => 9.76092208681491, - 'elevation' => 10.0, - 'time' => new \DateTime("+ 2 MINUTE") - ], - [ - 'latitude' => 54.73327743521187, - 'longitude' => 9.66187816543752, - 'elevation' => 42.42, - 'time' => new \DateTime("+ 3 MINUTE") - ], - [ - 'latitude' => 54.63342326167919, - 'longitude' => 9.562439849679859, - 'elevation' => 12, - 'time' => new \DateTime("+ 4 MINUTE") - ] - ]; - - // Creating sample link object for metadata - $link = new Link(); - $link->href = "https://sibyx.github.io/phpgpx"; - $link->text = 'phpGPX Docs'; - - // GpxFile contains data and handles serialization of objects - $gpx_file = new GpxFile(); - - // Creating sample Metadata object - $gpx_file->metadata = new Metadata(); - - // Time attribute is always \DateTime object! - $gpx_file->metadata->time = new \DateTime(); - - // Description of GPX file - $gpx_file->metadata->description = "My pretty awesome GPX file, created using phpGPX library!"; - - // Adding link created before to links array of metadata - // Metadata of GPX file can contain more than one link - $gpx_file->metadata->links[] = $link; - - // Creating track - $track = new Track(); - - // Name of track - $track->name = sprintf("Some random points in logical order. Input array should be already ordered!"); - - // Type of data stored in track - $track->type = 'RUN'; - - // Source of GPS coordinates - $track->source = sprintf("MySpecificGarminDevice"); - - $wp = []; - foreach ($sample_data as $sample_point) { - // Creating trackpoint - $point = new Point(Point::WAYPOINT); - $point->latitude = $sample_point['latitude']; - $point->longitude = $sample_point['longitude']; - $point->elevation = $sample_point['elevation']; - $point->time = $sample_point['time']; - - $wp[] = $point; - } - - $gpx_file->waypoints = $wp; - - $gpx_file->save($this->waypoint_created_file, \phpGPX\phpGPX::XML_FORMAT); - } - - public function setUp(): void - { - $this->waypoint_created_file = dirname(__FILE__)."/waypoint_test.gpx"; - $this->waypoint_saved_file = dirname(__FILE__).'/output_waypoint_test.gpx'; - // remove any test file hanging around - system("rm -f {$this->waypoint_created_file}"); - // now create the test file - $this->createWaypointFile(); - } - public function tearDown(): void - { - system("rm -f {$this->waypoint_created_file}"); - system("rm -f {$this->waypoint_saved_file}"); - } - - /** - * @coversNothing - * @return void - */ - public function test_waypoints_load() - { - $origFile = $this->waypoint_created_file; - $outFile = $this->waypoint_saved_file; - - $gpx = new phpGPX(); - $file = $gpx->load($origFile); - - phpGPX::$PRETTY_PRINT = true; - $file->save($outFile, phpGPX::XML_FORMAT); - - $retcode = 0; - system("diff $origFile $outFile", $retcode); - // system("diff $origFile $outFile2", $retcode); - $this->assertEquals($retcode, 0); - } -} diff --git a/src/phpGPX/Tests/LoadFileTest.php b/src/phpGPX/Tests/LoadFileTest.php index 37f5104..807ca71 100644 --- a/src/phpGPX/Tests/LoadFileTest.php +++ b/src/phpGPX/Tests/LoadFileTest.php @@ -2,7 +2,7 @@ namespace phpGPX\Tests; -use phpGPX\phpGPX; +use phpGPX\phpGpx; use PHPUnit\Framework\TestCase; class LoadFileTest extends TestCase diff --git a/src/phpGPX/Tests/LoadRouteFileTest.php b/src/phpGPX/Tests/LoadRouteFileTest.php deleted file mode 100644 index d474684..0000000 --- a/src/phpGPX/Tests/LoadRouteFileTest.php +++ /dev/null @@ -1,190 +0,0 @@ - - */ - - -namespace phpGPX\Tests; - -use phpGPX\phpGPX; -use PHPUnit\Framework\TestCase; - -class LoadRouteFileTest extends TestCase -{ - /** - * @coversNothing - * @return void - */ - public function testRouteFile() - { - $file = __DIR__ . '/fixtures/route.gpx'; - - $gpx = new phpGpx(); - $gpxFile = $gpx->load($file); - - $this->assertEqualsWithDelta($this->createExpectedArray(), $gpxFile->toArray(),0.1); - - // Check XML generation - $gpxFile->toXML()->saveXML(); - } - - /** - * @coversNothing - * @return void - */ - public function testRouteFileWithSmoothedStats() - { - $file = __DIR__ . '/fixtures/gps-track.gpx'; - - $gpx = new phpGpx(); - $gpx::$APPLY_ELEVATION_SMOOTHING = true; - $gpx::$APPLY_DISTANCE_SMOOTHING = true; - $gpxFile = $gpx->load($file); - - $this->assertEquals(6, $gpxFile->tracks[0]->stats->cumulativeElevationGain); - - - // this should give a higher number for the elevation - $gpx::$APPLY_ELEVATION_SMOOTHING = false; - $gpx::$APPLY_DISTANCE_SMOOTHING = false; - $gpxFile = $gpx->load($file); - - $this->assertEquals(6.12, number_format($gpxFile->tracks[0]->stats->cumulativeElevationGain, 2)); - } - - private function createExpectedArray() - { - return [ - 'creator' => 'RouteConverter', - 'metadata' => [ - 'name' => 'Test file by Patrick', - ], - 'routes' => [ - [ - 'name' => "Patrick's Route", - 'rtep' => [ - [ - 'lat' => 54.9328621088893, - 'lon' => 9.860624216140083, - 'ele' => 0.0, - 'name' => 'Position 1', - ], - [ - 'lat' => 54.93293237320851, - 'lon' => 9.86092208681491, - 'ele' => 1.0, - 'name' => 'Position 2', - 'difference' => 20.57107116626639, - 'distance' => 20.57107116626639, - ], - [ - 'lat' => 54.93327743521187, - 'lon' => 9.86187816543752, - 'ele' => 2.0, - 'name' => 'Position 3', - 'difference' => 72.1308280496404, - 'distance' => 92.70189921590679, - ], - [ - 'lat' => 54.93342326167919, - 'lon' => 9.862439849679859, - 'ele' => 3.0, - 'name' => 'Position 4', - 'difference' => 39.37668614018047, - 'distance' => 132.07858535608727, - ], - ], - 'stats' => [ - 'distance' => 132.0785853, - 'realDistance' => 132.0785853, - 'avgSpeed' => 0.0, - 'avgPace' => 0.0, - 'minAltitude' => 0.0, - 'minAltitudeCoords' => [ - 'lat' => 54.9328621088893, - 'lng' => 9.860624216140083 - ], - 'maxAltitude' => 3.0, - 'maxAltitudeCoords' => [ - 'lat' => 54.93342326167919, - 'lng' => 9.862439849679859 - ], - 'cumulativeElevationGain' => 3.0, - 'cumulativeElevationLoss' => 0.0, - 'duration' => 0.0, - 'startedAtCoords' => [ - 'lat' => 54.9328621088893, - 'lng' => 9.860624216140083 - ], - 'finishedAtCoords' => [ - 'lat' => 54.93342326167919, - 'lng' => 9.862439849679859 - ] - ] - ], - [ - 'name' => "Sibyx's Route", - 'rtep' => [ - [ - 'lat' => 54.9328621088893, - 'lon' => 9.860624216140083, - 'ele' => 0.0, - 'name' => 'Position 4', - ], - [ - 'lat' => 54.93293237320851, - 'lon' => 9.86092208681491, - 'ele' => 1.0, - 'name' => 'Position 3', - 'difference' => 20.57107116626639, - 'distance' => 20.57107116626639, - ], - [ - 'lat' => 54.93327743521187, - 'lon' => 9.86187816543752, - 'ele' => 2.0, - 'name' => 'Position 2', - 'difference' => 72.1308280496404, - 'distance' => 92.70189921590679, - ], - [ - 'lat' => 54.93342326167919, - 'lon' => 9.862439849679859, - 'ele' => 3.0, - 'name' => 'Position 1', - 'difference' => 39.37668614018047, - 'distance' => 132.07858535608727, - ], - ], - 'stats' => [ - 'distance' => 132.0785853, - 'realDistance' => 132.0785853, - 'avgSpeed' => 0.0, - 'avgPace' => 0.0, - 'minAltitude' => 0.0, - 'minAltitudeCoords' => [ - 'lat' => 54.9328621088893, - 'lng' => 9.860624216140083 - ], - 'maxAltitude' => 3.0, - 'maxAltitudeCoords' => [ - 'lat' => 54.93342326167919, - 'lng' => 9.862439849679859 - ], - 'cumulativeElevationGain' => 3.0, - 'cumulativeElevationLoss' => 0.0, - 'duration' => 0.0, - 'startedAtCoords' => [ - 'lat' => 54.9328621088893, - 'lng' => 9.860624216140083 - ], - 'finishedAtCoords' => [ - 'lat' => 54.93342326167919, - 'lng' => 9.862439849679859 - ] - ] - ] - ], - ]; - } -} diff --git a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php index d465bb2..2d43f83 100644 --- a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php +++ b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php @@ -23,6 +23,7 @@ protected function setUp(): void 49.090543, 18.886939 ); + // Input file $this->file = simplexml_load_file(sprintf("%s/bounds.xml", __DIR__)); } @@ -44,7 +45,7 @@ public function testParse() $this->assertEquals($this->bounds->minLatitude, $bounds->minLatitude); $this->assertEquals($this->bounds->minLongitude, $bounds->minLongitude); - $this->assertEquals($this->bounds->toArray(), $bounds->toArray()); + $this->assertEquals($this->bounds->jsonSerialize(), $bounds->jsonSerialize()); } /** @@ -64,15 +65,4 @@ public function testToXML() $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); } - - /** - * @covers \phpGPX\Models\Bounds - * @return void - */ - public function testToJSON() - { - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/bounds.json", __DIR__), json_encode($this->bounds->toArray()) - ); - } } diff --git a/src/phpGPX/Tests/Parsers/Bounds/bounds.json b/src/phpGPX/Tests/Parsers/Bounds/bounds.json index 2b529a2..e91dc9a 100644 --- a/src/phpGPX/Tests/Parsers/Bounds/bounds.json +++ b/src/phpGPX/Tests/Parsers/Bounds/bounds.json @@ -1,6 +1 @@ -{ - "maxlat": 49.090543, - "maxlon": 18.886939, - "minlat": 49.072489, - "minlon": 18.814543 -} \ No newline at end of file +[ 18.814543, 49.072489, 18.886939, 49.090543 ] \ No newline at end of file diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index 5d91fb9..44d06f5 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -22,89 +22,14 @@ class phpGPX const XML_FORMAT = 'xml'; const PACKAGE_NAME = 'phpGPX'; - const VERSION = '1.3.0'; - - /** - * Create Stats object for each track, segment and route - * @var bool - */ - public static $CALCULATE_STATS = true; - - /** - * Additional sort based on timestamp in Routes & Tracks on XML read. - * Disabled by default, data should be already sorted. - * @var bool - */ - public static $SORT_BY_TIMESTAMP = false; - - /** - * Default DateTime output format in JSON serialization. - * @var string - */ - public static $DATETIME_FORMAT = 'c'; - - /** - * Default timezone for display. - * Data are always stored in UTC timezone. - * @var string - */ - public static $DATETIME_TIMEZONE_OUTPUT = 'UTC'; - - /** - * Pretty print. - * @var bool - */ - public static $PRETTY_PRINT = true; - - /** - * In stats elevation calculation: ignore points with an elevation of 0 - * This can happen with some GPS software adding a point with 0 elevation - * - * @var bool - */ - public static $IGNORE_ELEVATION_0 = true; - - /** - * Apply elevation gain/loss smoothing? If true, the threshold in - * ELEVATION_SMOOTHING_THRESHOLD and ELEVATION_SMOOTHING_SPIKES_THRESHOLD (if not null) applies - * @var bool - */ - public static $APPLY_ELEVATION_SMOOTHING = false; - - /** - * if APPLY_ELEVATION_SMOOTHING is true - * the minimum elevation difference between considered points in meters - * @var int - */ - public static $ELEVATION_SMOOTHING_THRESHOLD = 2; - - /** - * if APPLY_ELEVATION_SMOOTHING is true - * the maximum elevation difference between considered points in meters - * @var int|null - */ - public static $ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; - - /** - * Apply distance calculation smoothing? If true, the threshold in - * DISTANCE_SMOOTHING_THRESHOLD applies - * @var bool - */ - public static $APPLY_DISTANCE_SMOOTHING = false; - - /** - * if APPLY_DISTANCE_SMOOTHING is true - * the minimum distance between considered points in meters - * @var int - */ - public static $DISTANCE_SMOOTHING_THRESHOLD = 2; + const VERSION = '2.0.0-alpha.1'; /** * Load GPX file. - * @param $path + * @param string $path * @return GpxFile */ - public static function load($path) + public static function load(string $path): GpxFile { $xml = file_get_contents($path); @@ -113,10 +38,10 @@ public static function load($path) /** * Parse GPX data string. - * @param $xml + * @param string $xml * @return GpxFile */ - public static function parse($xml) + public static function parse(string $xml): GpxFile { $xml = simplexml_load_string($xml); @@ -144,7 +69,7 @@ public static function parse($xml) * Create library signature from name and version. * @return string */ - public static function getSignature() + public static function getSignature(): string { return sprintf("%s/%s", self::PACKAGE_NAME, self::VERSION); } diff --git a/tests/fixtures/basic.gpx b/tests/fixtures/basic.gpx new file mode 100644 index 0000000..4c5ed35 --- /dev/null +++ b/tests/fixtures/basic.gpx @@ -0,0 +1,12763 @@ + + + + + Basic GPX Scenario + + gpx.studio + + + + + + + 390.4 + Zrnko + My Comment + My Description + + + + 397.0 + Fresh + Fresh comment + Fresh description + + + + Patrick's Route + + 395 + + + 408 + + + 419 + + + 459 + + + 531 + + + 685 + + + 816 + + + + + Hike + hiking + + + 223.3 + + + + 126 + + + + + 223.3 + + + + 126 + + + + + 221.7 + + + + 126 + + + + + 220.5 + + + + 126 + + + + + 219.4 + + + + 126 + + + + + 219.0 + + + + 126 + + + + + 217.9 + + + + 126 + + + + + 216.6 + + + + 126 + + + + + 215.4 + + + + 126 + + + + + 214.3 + + + + 126 + + + + + 213.2 + + + + 126 + + + + + 212.1 + + + + 126 + + + + + 211.0 + + + + 116 + + + + + 209.9 + + + + 115 + + + + + 208.9 + + + + 115 + + + + + 207.7 + + + + 115 + + + + + 206.8 + + + + 109 + + + + + 205.9 + + + + 114 + + + + + 205.1 + + + + 114 + + + + + 204.2 + + + + 114 + + + + + 203.2 + + + + 119 + + + + + 202.1 + + + + 117 + + + + + 201.1 + + + + 118 + + + + + 199.8 + + + + 116 + + + + + 198.6 + + + + 116 + + + + + 197.2 + + + + 116 + + + + + 195.9 + + + + 116 + + + + + 194.5 + + + + 115 + + + + + 192.2 + + + + 115 + + + + + 191.6 + + + + 110 + + + + + 190.9 + + + + 110 + + + + + 190.2 + + + + 110 + + + + + 189.3 + + + + 110 + + + + + 188.1 + + + + 110 + + + + + 186.7 + + + + 110 + + + + + 185.4 + + + + 110 + + + + + 184.2 + + + + 110 + + + + + 182.6 + + + + 110 + + + + + 181.5 + + + + 110 + + + + + 180.3 + + + + 140 + + + + + 179.4 + + + + 140 + + + + + 178.7 + + + + 143 + + + + + 178.1 + + + + 149 + + + + + 177.8 + + + + 149 + + + + + 177.6 + + + + 149 + + + + + 177.5 + + + + 149 + + + + + 177.4 + + + + 126 + + + + + 177.4 + + + + 126 + + + + + 177.4 + + + + 126 + + + + + 177.3 + + + + 126 + + + + + 177.3 + + + + 126 + + + + + 177.2 + + + + 126 + + + + + 177.2 + + + + 126 + + + + + 177.2 + + + + 126 + + + + + 177.0 + + + + 120 + + + + + 176.9 + + + + 120 + + + + + 176.8 + + + + 119 + + + + + 176.8 + + + + 119 + + + + + 176.5 + + + + 119 + + + + + 176.1 + + + + 119 + + + + + 175.5 + + + + 120 + + + + + 174.6 + + + + 123 + + + + + 173.8 + + + + 123 + + + + + 172.7 + + + + 123 + + + + + 171.6 + + + + 123 + + + + + 170.3 + + + + 123 + + + + + 169.4 + + + + 123 + + + + + 168.7 + + + + 123 + + + + + 168.1 + + + + 123 + + + + + 167.6 + + + + 123 + + + + + 167.3 + + + + 123 + + + + + 167.0 + + + + 123 + + + + + 166.8 + + + + 123 + + + + + 166.6 + + + + 113 + + + + + 166.4 + + + + 110 + + + + + 166.2 + + + + 111 + + + + + 166.0 + + + + 115 + + + + + 165.7 + + + + 113 + + + + + 165.2 + + + + 113 + + + + + 164.7 + + + + 112 + + + + + 164.3 + + + + 112 + + + + + 163.8 + + + + 112 + + + + + 163.4 + + + + 117 + + + + + 163.2 + + + + 117 + + + + + 163.0 + + + + 117 + + + + + 162.9 + + + + 115 + + + + + 162.9 + + + + 115 + + + + + 163.1 + + + + 115 + + + + + 163.3 + + + + 115 + + + + + 163.8 + + + + 115 + + + + + 164.1 + + + + 116 + + + + + 164.3 + + + + 116 + + + + + 164.8 + + + + 114 + + + + + 165.2 + + + + 114 + + + + + 165.6 + + + + 114 + + + + + 165.7 + + + + 110 + + + + + 165.8 + + + + 110 + + + + + 165.8 + + + + 109 + + + + + 166.2 + + + + 109 + + + + + 166.5 + + + + 109 + + + + + 166.7 + + + + 109 + + + + + 166.9 + + + + 116 + + + + + 166.9 + + + + 118 + + + + + 166.9 + + + + 118 + + + + + 166.9 + + + + 118 + + + + + 165.8 + + + + 118 + + + + + 164.7 + + + + 119 + + + + + 163.3 + + + + 119 + + + + + 161.8 + + + + 119 + + + + + 160.2 + + + + 115 + + + + + 158.4 + + + + 115 + + + + + 156.7 + + + + 112 + + + + + 155.1 + + + + 112 + + + + + 153.3 + + + + 112 + + + + + 151.6 + + + + 116 + + + + + 149.7 + + + + 116 + + + + + 148.2 + + + + 116 + + + + + 146.9 + + + + 116 + + + + + 145.7 + + + + 116 + + + + + 144.6 + + + + 107 + + + + + 143.8 + + + + 109 + + + + + 143.1 + + + + 109 + + + + + 142.3 + + + + 112 + + + + + 141.7 + + + + 112 + + + + + 141.1 + + + + 112 + + + + + 140.6 + + + + 112 + + + + + 140.1 + + + + 112 + + + + + 139.8 + + + + 112 + + + + + 139.4 + + + + 112 + + + + + 139.0 + + + + 112 + + + + + 138.7 + + + + 112 + + + + + 138.3 + + + + 112 + + + + + 138.1 + + + + 112 + + + + + 137.8 + + + + 112 + + + + + 137.6 + + + + 112 + + + + + 137.4 + + + + 112 + + + + + 137.2 + + + + 113 + + + + + 137.0 + + + + 114 + + + + + 136.7 + + + + 114 + + + + + 136.5 + + + + 117 + + + + + 136.3 + + + + 117 + + + + + 136.2 + + + + 117 + + + + + 136.0 + + + + 117 + + + + + 135.7 + + + + 117 + + + + + 135.4 + + + + 108 + + + + + 135.3 + + + + 108 + + + + + 135.3 + + + + 103 + + + + + 135.1 + + + + 103 + + + + + 135.0 + + + + 103 + + + + + 134.6 + + + + 103 + + + + + 134.4 + + + + 103 + + + + + 134.2 + + + + 103 + + + + + 134.1 + + + + 99 + + + + + 133.8 + + + + 99 + + + + + 133.2 + + + + 110 + + + + + 132.4 + + + + 113 + + + + + 131.4 + + + + 113 + + + + + 130.5 + + + + 113 + + + + + 129.7 + + + + 115 + + + + + 128.7 + + + + 114 + + + + + 127.7 + + + + 112 + + + + + 126.6 + + + + 112 + + + + + 125.6 + + + + 114 + + + + + 124.5 + + + + 116 + + + + + 123.8 + + + + 116 + + + + + 123.3 + + + + 105 + + + + + 122.9 + + + + 107 + + + + + 122.8 + + + + 107 + + + + + 122.8 + + + + 107 + + + + + 123.2 + + + + 100 + + + + + 123.8 + + + + 100 + + + + + 125.1 + + + + 100 + + + + + 126.5 + + + + 100 + + + + + 127.8 + + + + 100 + + + + + 129.0 + + + + 100 + + + + + 130.5 + + + + 100 + + + + + 131.6 + + + + 134 + + + + + 132.7 + + + + 134 + + + + + 132.8 + + + + 138 + + + + + 133.2 + + + + 138 + + + + + 134.4 + + + + 138 + + + + + 135.7 + + + + 138 + + + + + 137.5 + + + + 137 + + + + + 138.7 + + + + 135 + + + + + 139.7 + + + + 135 + + + + + 140.9 + + + + 137 + + + + + 141.2 + + + + 135 + + + + + 141.7 + + + + 128 + + + + + 142.2 + + + + 124 + + + + + 142.6 + + + + 124 + + + + + 143.0 + + + + 124 + + + + + 143.2 + + + + 124 + + + + + 143.2 + + + + 137 + + + + + 143.3 + + + + 139 + + + + + 143.3 + + + + 139 + + + + + 143.3 + + + + 132 + + + + + 143.3 + + + + 130 + + + + + 143.3 + + + + 130 + + + + + 143.3 + + + + 130 + + + + + 143.4 + + + + 130 + + + + + 143.3 + + + + 129 + + + + + 143.3 + + + + 129 + + + + + 143.3 + + + + 128 + + + + + 143.3 + + + + 128 + + + + + 143.3 + + + + 127 + + + + + 143.2 + + + + 127 + + + + + 143.2 + + + + 126 + + + + + 142.8 + + + + 120 + + + + + 142.2 + + + + 120 + + + + + 141.4 + + + + 120 + + + + + 140.4 + + + + 120 + + + + + 139.3 + + + + 96 + + + + + 138.2 + + + + 96 + + + + + 137.1 + + + + 96 + + + + + 135.4 + + + + 96 + + + + + 135.0 + + + + 98 + + + + + 134.7 + + + + 119 + + + + + 134.3 + + + + 119 + + + + + 134.0 + + + + 119 + + + + + 133.7 + + + + 119 + + + + + 133.4 + + + + 120 + + + + + 133.3 + + + + 120 + + + + + 133.2 + + + + 120 + + + + + 133.1 + + + + 120 + + + + + 133.0 + + + + 120 + + + + + 132.9 + + + + 123 + + + + + 132.9 + + + + 123 + + + + + 132.9 + + + + 122 + + + + + 132.8 + + + + 122 + + + + + 132.6 + + + + 122 + + + + + 132.6 + + + + 122 + + + + + 132.4 + + + + 122 + + + + + 132.3 + + + + 123 + + + + + 132.0 + + + + 122 + + + + + 131.8 + + + + 122 + + + + + 131.0 + + + + 122 + + + + + 130.8 + + + + 122 + + + + + 130.8 + + + + 122 + + + + + 130.5 + + + + 122 + + + + + 130.3 + + + + 122 + + + + + 130.3 + + + + 122 + + + + + 129.5 + + + + 92 + + + + + 128.4 + + + + 92 + + + + + 127.4 + + + + 92 + + + + + 126.5 + + + + 92 + + + + + 125.3 + + + + 94 + + + + + 124.2 + + + + 94 + + + + + 122.9 + + + + 94 + + + + + 121.6 + + + + 94 + + + + + 120.3 + + + + 90 + + + + + 119.0 + + + + 90 + + + + + 118.9 + + + + 90 + + + + + 117.2 + + + + 83 + + + + + 115.5 + + + + 83 + + + + + 114.0 + + + + 83 + + + + + 112.5 + + + + 93 + + + + + 110.7 + + + + 94 + + + + + 109.1 + + + + 94 + + + + + 107.5 + + + + 94 + + + + + 106.2 + + + + 84 + + + + + 105.0 + + + + 85 + + + + + 103.6 + + + + 85 + + + + + 102.7 + + + + 85 + + + + + 101.9 + + + + 93 + + + + + 101.2 + + + + 104 + + + + + 100.6 + + + + 106 + + + + + 100.3 + + + + 106 + + + + + 100.2 + + + + 92 + + + + + 100.1 + + + + 87 + + + + + 100.0 + + + + 86 + + + + + 99.9 + + + + 91 + + + + + 99.8 + + + + 93 + + + + + 99.6 + + + + 96 + + + + + 99.4 + + + + 96 + + + + + 99.3 + + + + 96 + + + + + 99.3 + + + + 96 + + + + + 99.4 + + + + 96 + + + + + 99.6 + + + + 96 + + + + + 99.7 + + + + 96 + + + + + 100.0 + + + + 100 + + + + + 100.3 + + + + 100 + + + + + 100.4 + + + + 100 + + + + + 100.4 + + + + 100 + + + + + 100.5 + + + + 106 + + + + + 100.6 + + + + 106 + + + + + 100.6 + + + + 106 + + + + + 100.9 + + + + 106 + + + + + 101.2 + + + + 111 + + + + + 102.0 + + + + 111 + + + + + 102.2 + + + + 111 + + + + + 102.4 + + + + 111 + + + + + 102.4 + + + + 111 + + + + + 102.6 + + + + 111 + + + + + 102.7 + + + + 111 + + + + + 102.8 + + + + 111 + + + + + 103.0 + + + + 113 + + + + + 103.2 + + + + 113 + + + + + 103.5 + + + + 112 + + + + + 103.7 + + + + 110 + + + + + 104.3 + + + + 111 + + + + + 104.7 + + + + 111 + + + + + 104.8 + + + + 110 + + + + + 105.0 + + + + 110 + + + + + 105.1 + + + + 110 + + + + + 105.1 + + + + 112 + + + + + 105.1 + + + + 112 + + + + + 106.5 + + + + 109 + + + + + 108.0 + + + + 109 + + + + + 109.1 + + + + 103 + + + + + 110.2 + + + + 103 + + + + + 111.1 + + + + 121 + + + + + 111.8 + + + + 119 + + + + + 112.5 + + + + 128 + + + + + 112.7 + + + + 128 + + + + + 112.9 + + + + 130 + + + + + 113.0 + + + + 130 + + + + + 113.1 + + + + 131 + + + + + 113.3 + + + + 131 + + + + + 113.4 + + + + 132 + + + + + 113.5 + + + + 132 + + + + + 113.8 + + + + 131 + + + + + 113.8 + + + + 130 + + + + + 113.9 + + + + 130 + + + + + 114.0 + + + + 131 + + + + + 114.0 + + + + 131 + + + + + 114.0 + + + + 129 + + + + + 114.1 + + + + 129 + + + + + 114.1 + + + + 130 + + + + + 114.1 + + + + 129 + + + + + 114.1 + + + + 129 + + + + + 114.2 + + + + 130 + + + + + 114.3 + + + + 130 + + + + + 114.4 + + + + 130 + + + + + 114.4 + + + + 130 + + + + + 114.5 + + + + 129 + + + + + 114.7 + + + + 127 + + + + + 114.8 + + + + 124 + + + + + 114.9 + + + + 124 + + + + + 115.5 + + + + 124 + + + + + 116.1 + + + + 119 + + + + + 116.9 + + + + 119 + + + + + 117.8 + + + + 88 + + + + + 117.9 + + + + 88 + + + + + 118.2 + + + + 88 + + + + + 118.6 + + + + 86 + + + + + 118.8 + + + + 86 + + + + + 119.1 + + + + 87 + + + + + 119.2 + + + + 86 + + + + + 119.5 + + + + 87 + + + + + 119.8 + + + + 86 + + + + + 120.1 + + + + 86 + + + + + 120.3 + + + + 88 + + + + + 120.6 + + + + 88 + + + + + 120.7 + + + + 88 + + + + + 121.3 + + + + 88 + + + + + 121.4 + + + + 88 + + + + + 121.6 + + + + 88 + + + + + 121.9 + + + + 91 + + + + + 122.2 + + + + 91 + + + + + 122.4 + + + + 92 + + + + + 122.5 + + + + 92 + + + + + 122.8 + + + + 92 + + + + + 123.0 + + + + 92 + + + + + 123.1 + + + + 92 + + + + + 124.0 + + + + 90 + + + + + 124.9 + + + + 129 + + + + + 125.7 + + + + 134 + + + + + 126.3 + + + + 134 + + + + + 126.8 + + + + 134 + + + + + 127.3 + + + + 139 + + + + + 127.4 + + + + 139 + + + + + 127.4 + + + + 139 + + + + + 127.4 + + + + 139 + + + + + 127.4 + + + + 139 + + + + + 127.5 + + + + 139 + + + + + 127.5 + + + + 139 + + + + + 127.6 + + + + 135 + + + + + 127.7 + + + + 135 + + + + + 127.7 + + + + 135 + + + + + 127.9 + + + + 135 + + + + + 128.3 + + + + 134 + + + + + 128.3 + + + + 134 + + + + + 128.5 + + + + 133 + + + + + 128.6 + + + + 133 + + + + + 128.6 + + + + 133 + + + + + 128.8 + + + + 131 + + + + + 128.9 + + + + 131 + + + + + 129.0 + + + + 131 + + + + + 129.1 + + + + 130 + + + + + 129.3 + + + + 130 + + + + + 129.6 + + + + 130 + + + + + 129.6 + + + + 130 + + + + + 129.6 + + + + 133 + + + + + 129.7 + + + + 133 + + + + + 129.8 + + + + 133 + + + + + 129.8 + + + + 133 + + + + + 129.9 + + + + 132 + + + + + 130.1 + + + + 132 + + + + + 130.3 + + + + 132 + + + + + 130.4 + + + + 132 + + + + + 130.6 + + + + 134 + + + + + 130.7 + + + + 134 + + + + + 130.7 + + + + 134 + + + + + 130.8 + + + + 137 + + + + + 130.9 + + + + 137 + + + + + 131.0 + + + + 138 + + + + + 131.1 + + + + 138 + + + + + 131.1 + + + + 138 + + + + + 131.1 + + + + 138 + + + + + 131.2 + + + + 138 + + + + + 131.2 + + + + 138 + + + + + 131.1 + + + + 132 + + + + + 131.0 + + + + 129 + + + + + 130.6 + + + + 129 + + + + + 130.5 + + + + 126 + + + + + 130.4 + + + + 126 + + + + + 130.3 + + + + 126 + + + + + 130.2 + + + + 126 + + + + + 130.1 + + + + 126 + + + + + 129.9 + + + + 126 + + + + + 129.9 + + + + 126 + + + + + 129.9 + + + + 126 + + + + + 129.8 + + + + 126 + + + + + 129.8 + + + + 126 + + + + + 129.8 + + + + 126 + + + + + 129.7 + + + + 128 + + + + + 129.6 + + + + 128 + + + + + 129.5 + + + + 127 + + + + + 129.5 + + + + 127 + + + + + 129.4 + + + + 128 + + + + + 129.3 + + + + 128 + + + + + 129.2 + + + + 128 + + + + + 129.1 + + + + 128 + + + + + 127.0 + + + + 121 + + + + + 126.1 + + + + 121 + + + + + 125.0 + + + + 121 + + + + + 124.1 + + + + 121 + + + + + 123.0 + + + + 121 + + + + + 122.0 + + + + 121 + + + + + 120.9 + + + + 109 + + + + + 119.6 + + + + 109 + + + + + 118.2 + + + + 109 + + + + + 116.9 + + + + 98 + + + + + 115.6 + + + + 101 + + + + + 114.4 + + + + 101 + + + + + 113.4 + + + + 101 + + + + + 112.4 + + + + 114 + + + + + 111.4 + + + + 114 + + + + + 110.5 + + + + 112 + + + + + 109.7 + + + + 112 + + + + + 108.8 + + + + 112 + + + + + 107.9 + + + + 112 + + + + + 107.0 + + + + 112 + + + + + 106.0 + + + + 112 + + + + + 105.1 + + + + 112 + + + + + 104.1 + + + + 112 + + + + + 103.2 + + + + 111 + + + + + 102.3 + + + + 107 + + + + + 101.7 + + + + 102 + + + + + 101.3 + + + + 100 + + + + + 101.0 + + + + 100 + + + + + 100.9 + + + + 100 + + + + + 100.8 + + + + 100 + + + + + 100.8 + + + + 100 + + + + + 100.8 + + + + 100 + + + + + 100.9 + + + + 100 + + + + + 101.0 + + + + 100 + + + + + 101.1 + + + + 100 + + + + + 101.4 + + + + 100 + + + + + 101.7 + + + + 101 + + + + + 101.8 + + + + 101 + + + + + 102.0 + + + + 101 + + + + + 102.1 + + + + 100 + + + + + 102.3 + + + + 100 + + + + + 102.4 + + + + 100 + + + + + 102.5 + + + + 101 + + + + + 102.6 + + + + 101 + + + + + 102.7 + + + + 102 + + + + + 102.8 + + + + 102 + + + + + 102.8 + + + + 102 + + + + + 102.9 + + + + 103 + + + + + 103.0 + + + + 103 + + + + + 103.0 + + + + 103 + + + + + 103.1 + + + + 103 + + + + + 103.2 + + + + 104 + + + + + 103.2 + + + + 104 + + + + + 103.2 + + + + 105 + + + + + 103.2 + + + + 104 + + + + + 103.3 + + + + 104 + + + + + 103.3 + + + + 104 + + + + + 103.4 + + + + 104 + + + + + 103.4 + + + + 104 + + + + + 103.4 + + + + 104 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.4 + + + + 97 + + + + + 103.3 + + + + 97 + + + + + 103.2 + + + + 113 + + + + + 103.1 + + + + 113 + + + + + 103.1 + + + + 113 + + + + + 103.0 + + + + 113 + + + + + 103.0 + + + + 114 + + + + + 102.9 + + + + 114 + + + + + 102.9 + + + + 113 + + + + + 102.8 + + + + 113 + + + + + 102.7 + + + + 113 + + + + + 102.7 + + + + 113 + + + + + 102.6 + + + + 115 + + + + + 102.5 + + + + 115 + + + + + 102.5 + + + + 116 + + + + + 102.4 + + + + 116 + + + + + 102.2 + + + + 109 + + + + + 102.1 + + + + 109 + + + + + 102.1 + + + + 107 + + + + + 102.0 + + + + 107 + + + + + 102.0 + + + + 107 + + + + + 101.9 + + + + 107 + + + + + 101.9 + + + + 108 + + + + + 101.9 + + + + 108 + + + + + 101.8 + + + + 108 + + + + + 101.8 + + + + 108 + + + + + 101.8 + + + + 104 + + + + + 101.8 + + + + 104 + + + + + 101.8 + + + + 104 + + + + + 101.7 + + + + 104 + + + + + 101.7 + + + + 104 + + + + + 101.7 + + + + 100 + + + + + 101.7 + + + + 100 + + + + + 101.7 + + + + 98 + + + + + 101.7 + + + + 98 + + + + + 101.7 + + + + 98 + + + + + 101.7 + + + + 98 + + + + + 101.7 + + + + 98 + + + + + 101.8 + + + + 101 + + + + + 101.8 + + + + 101 + + + + + 101.8 + + + + 101 + + + + + 101.9 + + + + 101 + + + + + 102.0 + + + + 101 + + + + + 102.0 + + + + 101 + + + + + 102.0 + + + + 103 + + + + + 102.1 + + + + 103 + + + + + 102.1 + + + + 103 + + + + + 102.2 + + + + 103 + + + + + 102.2 + + + + 103 + + + + + 102.2 + + + + 105 + + + + + 102.3 + + + + 105 + + + + + 102.4 + + + + 105 + + + + + 102.5 + + + + 105 + + + + + 102.9 + + + + 106 + + + + + 103.0 + + + + 106 + + + + + 103.3 + + + + 105 + + + + + 103.6 + + + + 105 + + + + + 103.8 + + + + 105 + + + + + 104.5 + + + + 104 + + + + + 104.7 + + + + 105 + + + + + 105.0 + + + + 104 + + + + + 105.3 + + + + 104 + + + + + 105.4 + + + + 102 + + + + + 105.6 + + + + 102 + + + + + 105.7 + + + + 100 + + + + + 105.9 + + + + 100 + + + + + 106.0 + + + + 100 + + + + + 106.2 + + + + 100 + + + + + 106.4 + + + + 98 + + + + + 106.6 + + + + 99 + + + + + 106.7 + + + + 99 + + + + + 106.9 + + + + 108 + + + + + 107.0 + + + + 108 + + + + + 107.2 + + + + 113 + + + + + 107.3 + + + + 113 + + + + + 107.4 + + + + 114 + + + + + 107.5 + + + + 114 + + + + + 107.6 + + + + 114 + + + + + 107.7 + + + + 114 + + + + + 108.8 + + + + 114 + + + + + 109.3 + + + + 126 + + + + + 109.9 + + + + 126 + + + + + 110.5 + + + + 126 + + + + + + + 111.2 + + + + 126 + + + + + 112.0 + + + + 125 + + + + + 112.9 + + + + 125 + + + + + 114.0 + + + + 125 + + + + + 115.1 + + + + 125 + + + + + 116.0 + + + + 125 + + + + + 116.8 + + + + 131 + + + + + 117.5 + + + + 131 + + + + + 117.9 + + + + 131 + + + + + 118.0 + + + + 142 + + + + + 117.8 + + + + 142 + + + + + 117.4 + + + + 142 + + + + + 116.5 + + + + 142 + + + + + 116.4 + + + + 129 + + + + + 116.2 + + + + 129 + + + + + 116.1 + + + + 128 + + + + + 116.0 + + + + 128 + + + + + 115.8 + + + + 128 + + + + + 115.8 + + + + 128 + + + + + 115.6 + + + + 126 + + + + + 115.4 + + + + 126 + + + + + 115.3 + + + + 125 + + + + + 115.2 + + + + 125 + + + + + 115.0 + + + + 125 + + + + + 114.9 + + + + 125 + + + + + 114.6 + + + + 124 + + + + + 114.5 + + + + 124 + + + + + 114.3 + + + + 122 + + + + + 114.2 + + + + 122 + + + + + 114.1 + + + + 121 + + + + + 112.6 + + + + 114 + + + + + 111.7 + + + + 112 + + + + + 110.8 + + + + 112 + + + + + 109.9 + + + + 112 + + + + + 109.0 + + + + 103 + + + + + 108.1 + + + + 103 + + + + + 107.1 + + + + 107 + + + + + 106.3 + + + + 107 + + + + + 105.5 + + + + 107 + + + + + 105.4 + + + + 107 + + + + + 105.3 + + + + 101 + + + + + 105.0 + + + + 101 + + + + + 104.9 + + + + 101 + + + + + 104.8 + + + + 101 + + + + + 104.7 + + + + 101 + + + + + 104.5 + + + + 101 + + + + + 104.4 + + + + 101 + + + + + 104.3 + + + + 101 + + + + + 104.1 + + + + 100 + + + + + 101.7 + + + + 100 + + + + + 100.8 + + + + 100 + + + + + 100.0 + + + + 100 + + + + + 99.1 + + + + 107 + + + + + 98.3 + + + + 104 + + + + + 97.4 + + + + 109 + + + + + 95.9 + + + + 108 + + + + + 94.1 + + + + 114 + + + + + 92.5 + + + + 114 + + + + + 90.9 + + + + 114 + + + + + 89.3 + + + + 114 + + + + + 88.1 + + + + 114 + + + + + 86.8 + + + + 114 + + + + + 85.7 + + + + 120 + + + + + 84.8 + + + + 120 + + + + + 83.9 + + + + 118 + + + + + 83.2 + + + + 107 + + + + + 82.7 + + + + 107 + + + + + 82.3 + + + + 107 + + + + + 82.1 + + + + 107 + + + + + 81.9 + + + + 107 + + + + + 81.7 + + + + 109 + + + + + 81.5 + + + + 108 + + + + + 81.2 + + + + 108 + + + + + 80.8 + + + + 96 + + + + + 80.2 + + + + 96 + + + + + 79.3 + + + + 96 + + + + + 79.2 + + + + 95 + + + + + 79.1 + + + + 95 + + + + + 78.5 + + + + 95 + + + + + 78.2 + + + + 95 + + + + + 77.9 + + + + 94 + + + + + 77.7 + + + + 94 + + + + + 75.4 + + + + 112 + + + + + 75.0 + + + + 112 + + + + + 74.6 + + + + 112 + + + + + 74.1 + + + + 112 + + + + + 73.6 + + + + 112 + + + + + 73.1 + + + + 112 + + + + + 72.7 + + + + 112 + + + + + 72.6 + + + + 113 + + + + + 72.3 + + + + 113 + + + + + 72.1 + + + + 111 + + + + + 71.8 + + + + 111 + + + + + 71.6 + + + + 110 + + + + + 71.2 + + + + 110 + + + + + 71.2 + + + + 110 + + + + + 71.1 + + + + 110 + + + + + 70.7 + + + + 110 + + + + + 70.6 + + + + 107 + + + + + 70.4 + + + + 107 + + + + + 70.2 + + + + 104 + + + + + 70.1 + + + + 104 + + + + + 69.9 + + + + 102 + + + + + 69.5 + + + + 102 + + + + + 69.4 + + + + 102 + + + + + 69.2 + + + + 102 + + + + + 69.1 + + + + 102 + + + + + 68.9 + + + + 104 + + + + + 68.8 + + + + 104 + + + + + 68.7 + + + + 104 + + + + + 68.4 + + + + 104 + + + + + 68.2 + + + + 104 + + + + + 68.1 + + + + 103 + + + + + 67.9 + + + + 103 + + + + + 67.8 + + + + 103 + + + + + 67.6 + + + + 103 + + + + + 67.5 + + + + 104 + + + + + 67.3 + + + + 104 + + + + + 67.2 + + + + 103 + + + + + 67.1 + + + + 103 + + + + + 67.0 + + + + 100 + + + + + 66.8 + + + + 100 + + + + + 66.7 + + + + 98 + + + + + 66.6 + + + + 98 + + + + + 66.5 + + + + 98 + + + + + 66.4 + + + + 98 + + + + + 66.3 + + + + 100 + + + + + 66.2 + + + + 101 + + + + + 66.1 + + + + 100 + + + + + 66.0 + + + + 102 + + + + + 65.8 + + + + 102 + + + + + 65.7 + + + + 102 + + + + + 65.4 + + + + 102 + + + + + 65.2 + + + + 102 + + + + + 65.1 + + + + 102 + + + + + 64.7 + + + + 102 + + + + + 64.5 + + + + 104 + + + + + 64.4 + + + + 104 + + + + + 64.3 + + + + 104 + + + + + 64.1 + + + + 104 + + + + + 64.0 + + + + 104 + + + + + 62.9 + + + + 104 + + + + + 61.8 + + + + 104 + + + + + 60.9 + + + + 104 + + + + + 60.2 + + + + 104 + + + + + 59.6 + + + + 104 + + + + + 59.1 + + + + 104 + + + + + 58.6 + + + + 104 + + + + + 58.1 + + + + 104 + + + + + 57.6 + + + + 107 + + + + + 57.0 + + + + 107 + + + + + 56.4 + + + + 107 + + + + + 55.8 + + + + 107 + + + + + 55.3 + + + + 107 + + + + + 54.9 + + + + 101 + + + + + 54.7 + + + + 103 + + + + + 54.8 + + + + 102 + + + + + 55.0 + + + + 101 + + + + + 55.3 + + + + 106 + + + + + 55.4 + + + + 106 + + + + + 55.4 + + + + 109 + + + + + 55.1 + + + + 109 + + + + + 54.3 + + + + 109 + + + + + 53.4 + + + + 131 + + + + + 52.4 + + + + 126 + + + + + 51.2 + + + + 126 + + + + + 50.2 + + + + 114 + + + + + 49.0 + + + + 114 + + + + + 48.1 + + + + 114 + + + + + 47.1 + + + + 114 + + + + + 46.3 + + + + 114 + + + + + 45.4 + + + + 114 + + + + + 44.8 + + + + 114 + + + + + 44.1 + + + + 111 + + + + + 43.5 + + + + 111 + + + + + 42.8 + + + + 111 + + + + + 42.3 + + + + 98 + + + + + 42.0 + + + + 98 + + + + + 41.6 + + + + 98 + + + + + 41.5 + + + + 98 + + + + + 41.4 + + + + 98 + + + + + 41.3 + + + + 94 + + + + + 41.2 + + + + 92 + + + + + 41.0 + + + + 92 + + + + + 39.7 + + + + 92 + + + + + 38.5 + + + + 110 + + + + + 37.4 + + + + 110 + + + + + 36.2 + + + + 111 + + + + + 35.1 + + + + 111 + + + + + 34.0 + + + + 113 + + + + + 33.0 + + + + 113 + + + + + 32.2 + + + + 113 + + + + + 31.5 + + + + 112 + + + + + 30.7 + + + + 112 + + + + + 30.0 + + + + 112 + + + + + 29.2 + + + + 112 + + + + + 28.4 + + + + 112 + + + + + 28.0 + + + + 112 + + + + + 27.6 + + + + 112 + + + + + 27.2 + + + + 112 + + + + + 26.8 + + + + 112 + + + + + 26.3 + + + + 112 + + + + + 25.7 + + + + 125 + + + + + 25.0 + + + + 125 + + + + + 24.4 + + + + 125 + + + + + 23.8 + + + + 125 + + + + + 23.7 + + + + 119 + + + + + 24.2 + + + + 116 + + + + + 24.6 + + + + 120 + + + + + 25.0 + + + + 123 + + + + + 25.4 + + + + 127 + + + + + 25.6 + + + + 132 + + + + + 25.7 + + + + 132 + + + + + 25.6 + + + + 132 + + + + + 25.5 + + + + 132 + + + + + 25.3 + + + + 132 + + + + + 25.0 + + + + 132 + + + + + 24.6 + + + + 114 + + + + + 24.3 + + + + 115 + + + + + 23.9 + + + + 115 + + + + + 23.5 + + + + 115 + + + + + 23.1 + + + + 115 + + + + + 22.6 + + + + 113 + + + + + 22.1 + + + + 111 + + + + + 21.6 + + + + 110 + + + + + 21.0 + + + + 110 + + + + + 20.4 + + + + 110 + + + + + 19.9 + + + + 110 + + + + + 19.3 + + + + 111 + + + + + 18.6 + + + + 111 + + + + + 18.4 + + + + 111 + + + + + 18.3 + + + + 113 + + + + + 18.2 + + + + 113 + + + + + 18.1 + + + + 112 + + + + + 17.9 + + + + 112 + + + + + 17.8 + + + + 112 + + + + + 17.6 + + + + 112 + + + + + 17.4 + + + + 112 + + + + + 17.3 + + + + 112 + + + + + 17.1 + + + + 112 + + + + + 17.0 + + + + 112 + + + + + 16.9 + + + + 112 + + + + + 16.8 + + + + 112 + + + + + 16.6 + + + + 112 + + + + + 16.5 + + + + 114 + + + + + 16.4 + + + + 114 + + + + + 16.4 + + + + 114 + + + + + 16.3 + + + + 112 + + + + + 16.2 + + + + 113 + + + + + 16.2 + + + + 111 + + + + + 15.4 + + + + 111 + + + + + 15.0 + + + + 111 + + + + + 14.6 + + + + 111 + + + + + 14.2 + + + + 111 + + + + + 13.8 + + + + 94 + + + + + 13.4 + + + + 94 + + + + + 12.9 + + + + 94 + + + + + 12.4 + + + + 94 + + + + + 12.1 + + + + 94 + + + + + 11.6 + + + + 94 + + + + + 11.2 + + + + 94 + + + + + 11.7 + + + + 119 + + + + + 12.5 + + + + 125 + + + + + 13.3 + + + + 125 + + + + + 14.1 + + + + 125 + + + + + 14.9 + + + + 122 + + + + + 15.7 + + + + 122 + + + + + 17.1 + + + + 122 + + + + + 17.9 + + + + 122 + + + + + 19.0 + + + + 122 + + + + + 20.1 + + + + 122 + + + + + 21.1 + + + + 122 + + + + + 22.1 + + + + 122 + + + + + 22.9 + + + + 107 + + + + + 23.8 + + + + 107 + + + + + 24.4 + + + + 107 + + + + + 25.1 + + + + 107 + + + + + 25.6 + + + + 117 + + + + + 25.7 + + + + 117 + + + + + 25.5 + + + + 117 + + + + + 24.9 + + + + 120 + + + + + 24.1 + + + + 120 + + + + + 23.4 + + + + 121 + + + + + 24.1 + + + + 121 + + + + + 24.8 + + + + 120 + + + + + 25.5 + + + + 117 + + + + + 26.1 + + + + 117 + + + + + 26.6 + + + + 117 + + + + + 27.0 + + + + 115 + + + + + 27.4 + + + + 115 + + + + + 27.7 + + + + 115 + + + + + 28.1 + + + + 115 + + + + + 28.7 + + + + 115 + + + + + 29.4 + + + + 111 + + + + + 30.2 + + + + 104 + + + + + 31.1 + + + + 104 + + + + + 31.8 + + + + 104 + + + + + 32.5 + + + + 104 + + + + + 33.5 + + + + 104 + + + + + 34.5 + + + + 129 + + + + + 35.6 + + + + 129 + + + + + 36.8 + + + + 133 + + + + + 38.3 + + + + 131 + + + + + 39.4 + + + + 131 + + + + + 40.6 + + + + 137 + + + + + 41.2 + + + + 137 + + + + + 41.3 + + + + 137 + + + + + 41.4 + + + + 137 + + + + + 41.4 + + + + 137 + + + + + 41.6 + + + + 138 + + + + + 41.9 + + + + 138 + + + + + 42.3 + + + + 134 + + + + + 42.8 + + + + 134 + + + + + 43.4 + + + + 134 + + + + + 43.9 + + + + 134 + + + + + 44.5 + + + + 134 + + + + + 45.3 + + + + 132 + + + + + 46.0 + + + + 132 + + + + + 46.9 + + + + 132 + + + + + 47.9 + + + + 132 + + + + + 48.7 + + + + 132 + + + + + 49.8 + + + + 134 + + + + + 51.0 + + + + 135 + + + + + 52.0 + + + + 136 + + + + + 53.0 + + + + 136 + + + + + 53.9 + + + + 136 + + + + + 54.8 + + + + 136 + + + + + 55.3 + + + + 145 + + + + + 55.5 + + + + 145 + + + + + 55.4 + + + + 145 + + + + + 55.1 + + + + 145 + + + + + 54.9 + + + + 142 + + + + + 54.7 + + + + 142 + + + + + 54.7 + + + + 138 + + + + + 55.0 + + + + 131 + + + + + 55.4 + + + + 129 + + + + + 56.0 + + + + 130 + + + + + 56.5 + + + + 130 + + + + + 57.0 + + + + 130 + + + + + 57.7 + + + + 130 + + + + + 58.1 + + + + 133 + + + + + 58.7 + + + + 133 + + + + + 59.1 + + + + 133 + + + + + 59.6 + + + + 138 + + + + + 60.1 + + + + 138 + + + + + 60.2 + + + + 138 + + + + + 60.4 + + + + 133 + + + + + 60.7 + + + + 133 + + + + + 60.7 + + + + 133 + + + + + 60.9 + + + + 133 + + + + + 61.1 + + + + 133 + + + + + 61.4 + + + + 129 + + + + + 61.6 + + + + 129 + + + + + 61.8 + + + + 127 + + + + + 62.0 + + + + 127 + + + + + 62.3 + + + + 124 + + + + + 62.3 + + + + 124 + + + + + 62.6 + + + + 125 + + + + + 62.9 + + + + 125 + + + + + 63.0 + + + + 129 + + + + + 63.2 + + + + 132 + + + + + 63.3 + + + + 132 + + + + + 63.4 + + + + 132 + + + + + 63.6 + + + + 132 + + + + + 63.8 + + + + 134 + + + + + 66.2 + + + + 134 + + + + + 66.8 + + + + 134 + + + + + 67.4 + + + + 146 + + + + + 67.8 + + + + 146 + + + + + 68.6 + + + + 144 + + + + + 69.6 + + + + 144 + + + + + 70.6 + + + + 144 + + + + + 72.1 + + + + 144 + + + + + 73.6 + + + + 145 + + + + + 75.2 + + + + 145 + + + + + 76.7 + + + + 145 + + + + + 78.1 + + + + 156 + + + + + 79.5 + + + + 156 + + + + + 79.6 + + + + 156 + + + + + 79.7 + + + + 156 + + + + + 80.6 + + + + 158 + + + + + 80.8 + + + + 158 + + + + + 80.9 + + + + 157 + + + + + 81.0 + + + + 157 + + + + + 81.1 + + + + 157 + + + + + 81.2 + + + + 157 + + + + + 81.3 + + + + 155 + + + + + 81.4 + + + + 155 + + + + + 81.5 + + + + 155 + + + + + 81.6 + + + + 155 + + + + + 81.7 + + + + 155 + + + + + 81.7 + + + + 153 + + + + + 81.9 + + + + 153 + + + + + 81.9 + + + + 146 + + + + + 82.0 + + + + 146 + + + + + 82.0 + + + + 144 + + + + + 82.1 + + + + 144 + + + + + 82.1 + + + + 144 + + + + + 82.2 + + + + 144 + + + + + 82.2 + + + + 144 + + + + + 82.2 + + + + 144 + + + + + 82.3 + + + + 144 + + + + + 82.3 + + + + 144 + + + + + 82.4 + + + + 142 + + + + + 82.5 + + + + 142 + + + + + 82.6 + + + + 142 + + + + + 82.7 + + + + 142 + + + + + 82.9 + + + + 142 + + + + + 82.9 + + + + 142 + + + + + 83.5 + + + + 142 + + + + + 83.7 + + + + 110 + + + + + 84.0 + + + + 110 + + + + + 84.1 + + + + 110 + + + + + 84.2 + + + + 110 + + + + + 84.6 + + + + 110 + + + + + 84.7 + + + + 108 + + + + + 85.0 + + + + 108 + + + + + 85.2 + + + + 107 + + + + + 85.3 + + + + 107 + + + + + 85.4 + + + + 105 + + + + + 85.4 + + + + 103 + + + + + 85.7 + + + + 103 + + + + + 85.9 + + + + 103 + + + + + 86.0 + + + + 103 + + + + + 86.3 + + + + 105 + + + + + 86.5 + + + + 105 + + + + + 86.8 + + + + 105 + + + + + 87.0 + + + + 105 + + + + + 87.1 + + + + 106 + + + + + 87.3 + + + + 106 + + + + + 87.6 + + + + 106 + + + + + 87.9 + + + + 105 + + + + + 88.1 + + + + 105 + + + + + 89.6 + + + + 114 + + + + + 90.9 + + + + 142 + + + + + 91.0 + + + + 142 + + + + + 91.1 + + + + 141 + + + + + 91.5 + + + + 141 + + + + + 91.9 + + + + 143 + + + + + 92.4 + + + + 143 + + + + + 92.6 + + + + 144 + + + + + 92.9 + + + + 143 + + + + + 93.2 + + + + 143 + + + + + 93.5 + + + + 144 + + + + + 93.7 + + + + 144 + + + + + 93.8 + + + + 144 + + + + + 94.3 + + + + 144 + + + + + 94.5 + + + + 144 + + + + + 94.7 + + + + 144 + + + + + 95.2 + + + + 145 + + + + + 95.4 + + + + 145 + + + + + 95.5 + + + + 144 + + + + + 96.1 + + + + 146 + + + + + 96.3 + + + + 146 + + + + + 96.6 + + + + 146 + + + + + 97.6 + + + + 146 + + + + + 98.6 + + + + 152 + + + + + 99.6 + + + + 152 + + + + + 100.6 + + + + 156 + + + + + 101.6 + + + + 158 + + + + + 102.5 + + + + 158 + + + + + 103.2 + + + + 158 + + + + + 103.9 + + + + 158 + + + + + 103.9 + + + + 162 + + + + + 104.1 + + + + 162 + + + + + 104.1 + + + + 162 + + + + + 104.2 + + + + 162 + + + + + 104.4 + + + + 162 + + + + + 104.5 + + + + 162 + + + + + 104.6 + + + + 162 + + + + + 104.7 + + + + 163 + + + + + 104.8 + + + + 163 + + + + + 104.8 + + + + 163 + + + + + 104.9 + + + + 163 + + + + + 105.0 + + + + 161 + + + + + 105.3 + + + + 161 + + + + + 105.6 + + + + 161 + + + + + 106.4 + + + + 161 + + + + + 107.3 + + + + 158 + + + + + 108.5 + + + + 158 + + + + + 109.6 + + + + 158 + + + + + 110.6 + + + + 163 + + + + + 111.7 + + + + 164 + + + + + 112.6 + + + + 161 + + + + + 113.6 + + + + 161 + + + + + 114.5 + + + + 160 + + + + + 115.4 + + + + 160 + + + + + 116.3 + + + + 160 + + + + + 117.0 + + + + 163 + + + + + 117.6 + + + + 163 + + + + + 117.9 + + + + 158 + + + + + 117.9 + + + + 158 + + + + + 117.5 + + + + 158 + + + + + 116.7 + + + + 158 + + + + + 115.7 + + + + 158 + + + + + 114.7 + + + + 158 + + + + + 113.4 + + + + 148 + + + + + 112.4 + + + + 145 + + + + + 111.4 + + + + 136 + + + + + 110.4 + + + + 136 + + + + + 109.6 + + + + 127 + + + + + 108.9 + + + + 127 + + + + + 108.2 + + + + 127 + + + + + 107.7 + + + + 129 + + + + + 107.1 + + + + 129 + + + + + 106.3 + + + + 129 + + + + + 105.5 + + + + 129 + + + + + 104.6 + + + + 129 + + + + + 103.9 + + + + 129 + + + + + 103.2 + + + + 136 + + + + + 102.7 + + + + 136 + + + + + 102.2 + + + + 136 + + + + + 101.9 + + + + 136 + + + + + 101.7 + + + + 136 + + + + + 101.7 + + + + 122 + + + + + 101.8 + + + + 121 + + + + + 102.0 + + + + 127 + + + + + 102.5 + + + + 127 + + + + + 102.6 + + + + 131 + + + + + 102.6 + + + + 133 + + + + + 102.7 + + + + 133 + + + + + 102.8 + + + + 133 + + + + + 102.9 + + + + 133 + + + + + 103.0 + + + + 134 + + + + + 103.0 + + + + 134 + + + + + 103.1 + + + + 134 + + + + + 103.1 + + + + 134 + + + + + 103.1 + + + + 133 + + + + + 103.2 + + + + 134 + + + + + 103.3 + + + + 133 + + + + + 103.3 + + + + 134 + + + + + 103.3 + + + + 134 + + + + + 103.3 + + + + 134 + + + + + 103.4 + + + + 134 + + + + + 103.4 + + + + 134 + + + + + 103.3 + + + + 139 + + + + + 103.3 + + + + 139 + + + + + 103.2 + + + + 140 + + + + + 103.2 + + + + 140 + + + + + 103.1 + + + + 140 + + + + + 103.0 + + + + 140 + + + + + 103.0 + + + + 140 + + + + + 102.8 + + + + 140 + + + + + 102.3 + + + + 136 + + + + + 101.7 + + + + 136 + + + + + 101.2 + + + + 134 + + + + + 100.9 + + + + 134 + + + + + 100.8 + + + + 134 + + + + + 100.8 + + + + 134 + + + + + 101.1 + + + + 134 + + + + + 101.7 + + + + 134 + + + + + 102.6 + + + + 134 + + + + + 103.5 + + + + 130 + + + + + 104.5 + + + + 130 + + + + + 105.6 + + + + 130 + + + + + 106.6 + + + + 130 + + + + + 107.7 + + + + 134 + + + + + 108.6 + + + + 134 + + + + + 109.7 + + + + 150 + + + + + 110.8 + + + + 151 + + + + + 111.8 + + + + 155 + + + + + 113.0 + + + + 155 + + + + + 114.2 + + + + 155 + + + + + 115.5 + + + + 158 + + + + + 116.9 + + + + 157 + + + + + 118.3 + + + + 157 + + + + + 119.8 + + + + 157 + + + + + 121.2 + + + + 158 + + + + + 122.4 + + + + 158 + + + + + 123.5 + + + + 158 + + + + + 124.5 + + + + 158 + + + + + 125.6 + + + + 158 + + + + + 126.6 + + + + 158 + + + + + 127.9 + + + + 158 + + + + + 128.7 + + + + 158 + + + + + 129.4 + + + + 158 + + + + + 130.2 + + + + 158 + + + + + 130.8 + + + + 158 + + + + + 131.1 + + + + 158 + + + + + 131.1 + + + + 158 + + + + + 130.8 + + + + 157 + + + + + 130.3 + + + + 157 + + + + + 129.8 + + + + 157 + + + + + 129.2 + + + + 157 + + + + + 128.8 + + + + 157 + + + + + 128.2 + + + + 151 + + + + + 127.8 + + + + 151 + + + + + 127.3 + + + + 151 + + + + + 126.6 + + + + 151 + + + + + 125.9 + + + + 151 + + + + + 124.9 + + + + 151 + + + + + 124.0 + + + + 151 + + + + + 123.0 + + + + 133 + + + + + 121.7 + + + + 134 + + + + + 120.5 + + + + 134 + + + + + 119.1 + + + + 135 + + + + + 118.0 + + + + 135 + + + + + 117.0 + + + + 135 + + + + + 116.2 + + + + 138 + + + + + 115.5 + + + + 138 + + + + + 114.9 + + + + 131 + + + + + 114.5 + + + + 131 + + + + + 114.2 + + + + 131 + + + + + 113.9 + + + + 131 + + + + + 113.5 + + + + 131 + + + + + 113.0 + + + + 131 + + + + + 112.4 + + + + 131 + + + + + 111.6 + + + + 129 + + + + + 110.8 + + + + 131 + + + + + 109.6 + + + + 131 + + + + + 108.2 + + + + 131 + + + + + 106.7 + + + + 130 + + + + + 105.0 + + + + 129 + + + + + 103.6 + + + + 130 + + + + + 102.2 + + + + 127 + + + + + 101.3 + + + + 127 + + + + + 100.7 + + + + 127 + + + + + 100.3 + + + + 124 + + + + + 100.0 + + + + 121 + + + + + 99.7 + + + + 121 + + + + + 99.5 + + + + 121 + + + + + 99.4 + + + + 121 + + + + + 99.3 + + + + 124 + + + + + 99.4 + + + + 124 + + + + + 99.6 + + + + 124 + + + + + 99.8 + + + + 128 + + + + + 100.0 + + + + 125 + + + + + 100.1 + + + + 125 + + + + + 100.2 + + + + 125 + + + + + 100.3 + + + + 125 + + + + + 100.6 + + + + 125 + + + + + 101.1 + + + + 134 + + + + + 101.9 + + + + 136 + + + + + 103.0 + + + + 137 + + + + + 104.1 + + + + 137 + + + + + 105.6 + + + + 135 + + + + + 107.1 + + + + 135 + + + + + 108.6 + + + + 135 + + + + + 110.3 + + + + 135 + + + + + 112.0 + + + + 140 + + + + + 113.8 + + + + 140 + + + + + 115.8 + + + + 140 + + + + + 117.6 + + + + 140 + + + + + 119.3 + + + + 140 + + + + + 120.8 + + + + 140 + + + + + 122.1 + + + + 140 + + + + + 123.5 + + + + 140 + + + + + 124.7 + + + + 145 + + + + + 126.1 + + + + 161 + + + + + 127.2 + + + + 161 + + + + + 128.4 + + + + 164 + + + + + 129.4 + + + + 163 + + + + + 130.4 + + + + 163 + + + + + 131.5 + + + + 163 + + + + + 132.4 + + + + 163 + + + + + 133.1 + + + + 157 + + + + + 133.6 + + + + 157 + + + + + 134.0 + + + + 153 + + + + + 134.3 + + + + 153 + + + + + 134.7 + + + + 153 + + + + + 135.0 + + + + 151 + + + + + 135.7 + + + + 147 + + + + + 137.3 + + + + 152 + + + + + 139.0 + + + + 152 + + + + + 140.4 + + + + 159 + + + + + 141.7 + + + + 162 + + + + + 142.7 + + + + 159 + + + + + 143.2 + + + + 159 + + + + + 143.3 + + + + 159 + + + + + 143.1 + + + + 159 + + + + + 142.7 + + + + 159 + + + + + 142.2 + + + + 159 + + + + + 141.6 + + + + 143 + + + + + 140.9 + + + + 129 + + + + + 141.1 + + + + 129 + + + + + 141.3 + + + + 129 + + + + + 141.6 + + + + 129 + + + + + 142.0 + + + + 129 + + + + + 142.3 + + + + 129 + + + + + 142.7 + + + + 129 + + + + + 143.2 + + + + 125 + + + + + 143.9 + + + + 124 + + + + + 144.5 + + + + 124 + + + + + 145.2 + + + + 124 + + + + + 145.8 + + + + 124 + + + + + 146.4 + + + + 128 + + + + + 147.1 + + + + 128 + + + + + 147.6 + + + + 135 + + + + + 148.2 + + + + 135 + + + + + 148.8 + + + + 135 + + + + + 149.5 + + + + 141 + + + + + 150.2 + + + + 141 + + + + + 151.1 + + + + 141 + + + + + 152.1 + + + + 141 + + + + + 153.0 + + + + 141 + + + + + 153.9 + + + + 141 + + + + + 154.5 + + + + 141 + + + + + 155.0 + + + + 147 + + + + + 155.5 + + + + 147 + + + + + 155.9 + + + + 147 + + + + + 156.2 + + + + 147 + + + + + 156.6 + + + + 147 + + + + + 157.2 + + + + 147 + + + + + 157.8 + + + + 139 + + + + + 158.5 + + + + 139 + + + + + 159.4 + + + + 139 + + + + + 160.5 + + + + 140 + + + + + 161.7 + + + + 136 + + + + + 163.0 + + + + 137 + + + + + 164.4 + + + + 139 + + + + + 165.9 + + + + 139 + + + + + 167.7 + + + + 139 + + + + + 169.3 + + + + 143 + + + + + 170.8 + + + + 148 + + + + + 172.1 + + + + 148 + + + + + 172.9 + + + + 148 + + + + + 173.4 + + + + 148 + + + + + 173.7 + + + + 148 + + + + + 173.7 + + + + 156 + + + + + 173.6 + + + + 156 + + + + + 173.3 + + + + 156 + + + + + 172.9 + + + + 156 + + + + + 172.3 + + + + 156 + + + + + 171.7 + + + + 156 + + + + + 171.0 + + + + 156 + + + + + 170.5 + + + + 138 + + + + + 169.9 + + + + 138 + + + + + 169.4 + + + + 138 + + + + + 168.9 + + + + 138 + + + + + 168.4 + + + + 138 + + + + + 167.9 + + + + 138 + + + + + 167.5 + + + + 138 + + + + + 167.3 + + + + 138 + + + + + 167.1 + + + + 130 + + + + + 166.9 + + + + 128 + + + + + 166.7 + + + + 128 + + + + + 166.4 + + + + 123 + + + + + 166.1 + + + + 119 + + + + + 165.8 + + + + 119 + + + + + 165.5 + + + + 119 + + + + + 165.0 + + + + 119 + + + + + 164.7 + + + + 113 + + + + + 164.3 + + + + 113 + + + + + 163.8 + + + + 111 + + + + + 163.5 + + + + 111 + + + + + 163.2 + + + + 108 + + + + + 163.0 + + + + 108 + + + + + 162.9 + + + + 108 + + + + + 162.9 + + + + 107 + + + + + 163.0 + + + + 119 + + + + + 163.3 + + + + 120 + + + + + 163.7 + + + + 120 + + + + + 164.2 + + + + 120 + + + + + 164.7 + + + + 123 + + + + + 165.2 + + + + 123 + + + + + 165.6 + + + + 123 + + + + + 166.0 + + + + 140 + + + + + 166.2 + + + + 140 + + + + + 166.5 + + + + 140 + + + + + 166.6 + + + + 140 + + + + + 166.8 + + + + 140 + + + + + 167.0 + + + + 140 + + + + + 167.4 + + + + 141 + + + + + 167.8 + + + + 139 + + + + + 168.3 + + + + 139 + + + + + 169.0 + + + + 139 + + + + + 169.8 + + + + 137 + + + + + 170.8 + + + + 136 + + + + + 172.0 + + + + 135 + + + + + 173.2 + + + + 135 + + + + + 174.2 + + + + 135 + + + + + 175.1 + + + + 146 + + + + + 175.8 + + + + 146 + + + + + 176.3 + + + + 146 + + + + + 176.8 + + + + 146 + + + + + 177.1 + + + + 154 + + + + + 177.3 + + + + 155 + + + + + 177.4 + + + + 154 + + + + + 177.4 + + + + 149 + + + + + 177.4 + + + + 149 + + + + + 177.5 + + + + 149 + + + + + 177.6 + + + + 148 + + + + + 178.0 + + + + 148 + + + + + 178.6 + + + + 148 + + + + + 179.4 + + + + 144 + + + + + 180.2 + + + + 144 + + + + + 181.3 + + + + 144 + + + + + 182.6 + + + + 141 + + + + + 183.9 + + + + 141 + + + + + 185.4 + + + + 141 + + + + + 186.8 + + + + 141 + + + + + 188.1 + + + + 141 + + + + + 189.3 + + + + 141 + + + + + 190.2 + + + + 141 + + + + + 191.0 + + + + 141 + + + + + 191.8 + + + + 160 + + + + + 192.4 + + + + 160 + + + + + 193.5 + + + + 156 + + + + + 193.5 + + + + 156 + + + + + 195.3 + + + + 156 + + + + + 196.7 + + + + 149 + + + + + 198.3 + + + + 149 + + + + + 199.7 + + + + 149 + + + + + 201.0 + + + + 149 + + + + + 202.1 + + + + 150 + + + + + 203.2 + + + + 150 + + + + + 204.4 + + + + 150 + + + + + 205.5 + + + + 150 + + + + + 206.7 + + + + 147 + + + + + 207.6 + + + + 146 + + + + + 208.8 + + + + 143 + + + + + 209.9 + + + + 143 + + + + + 211.1 + + + + 144 + + + + + 212.2 + + + + 144 + + + + + 213.6 + + + + 144 + + + + + 215.0 + + + + 144 + + + + + 216.2 + + + + 144 + + + + + 217.6 + + + + 144 + + + + + 219.1 + + + + 146 + + + + + 220.3 + + + + 146 + + + + + 221.6 + + + + 146 + + + + + 222.9 + + + + 150 + + + + + 224.0 + + + + 150 + + + + + 225.1 + + + + 150 + + + + + 226.2 + + + + 150 + + + + + 227.1 + + + + 150 + + + + + 227.5 + + + + 150 + + + + + 227.8 + + + + 150 + + + + + + diff --git a/src/phpGPX/Tests/fixtures/gps-track.gpx b/tests/fixtures/gps-track.gpx similarity index 100% rename from src/phpGPX/Tests/fixtures/gps-track.gpx rename to tests/fixtures/gps-track.gpx diff --git a/tests/fixtures/hiking.gpx b/tests/fixtures/hiking.gpx new file mode 100644 index 0000000..8fc88c7 --- /dev/null +++ b/tests/fixtures/hiking.gpx @@ -0,0 +1,193 @@ + + + + + + + Trasa: Turčianske Kľačany → Strečno, stanica + + + 395 + + + 408 + + + 419 + + + 459 + + + 531 + + + 685 + + + 816 + + + 951 + + + 1002 + + + 1066 + + + 1123 + + + 1140 + + + 1140 + + + 1167 + + + 1174 + + + 1218 + + + 1225 + + + 1237 + + + 1252 + + + 1310 + + + 1315 + + + 1303 + + + 1303 + + + 1408 + + + 1468 + + + 1468 + + + 1300 + + + 1215 + + + 1215 + + + 1185 + + + 1160 + + + 1145 + + + 1145 + + + 1070 + + + 1070 + + + 1027 + + + 1018 + + + 999 + + + 975 + + + 964 + + + 882 + + + 749 + + + 646 + + + 567 + + + 507 + + + 479 + + + 425 + + + 425 + + + 403 + + + 385 + + + 372 + + + 371 + + + 371 + + + 371 + + + 361 + + + 358 + + + 356 + + + 355 + + + 355 + + + 360 + + + + + + \ No newline at end of file diff --git a/tests/fixtures/minimal.gpx b/tests/fixtures/minimal.gpx new file mode 100644 index 0000000..5b87f2c --- /dev/null +++ b/tests/fixtures/minimal.gpx @@ -0,0 +1,124 @@ + + + + Minimal GPX Scenario + + Jakub Dubec + + + + + + + Patrick's Route + + 0.0 + Position 1 + + + 1.0 + Position 2 + + + 2.0 + Position 3 + + + 3.0 + Position 4 + + + + + Hike + hiking + + + 215.4 + + + + 126 + + + + + 214.3 + + + + 126 + + + + + 213.2 + + + + 126 + + + + + 212.1 + + + + 126 + + + + + + + 117.8 + + + + 142 + + + + + 117.4 + + + + 142 + + + + + 116.5 + + + + 142 + + + + + 116.4 + + + + 129 + + + + + 116.2 + + + + 129 + + + + + + \ No newline at end of file diff --git a/src/phpGPX/Tests/fixtures/route.gpx b/tests/fixtures/route.gpx similarity index 100% rename from src/phpGPX/Tests/fixtures/route.gpx rename to tests/fixtures/route.gpx diff --git a/src/phpGPX/Tests/fixtures/timezero.gpx b/tests/fixtures/timezero.gpx similarity index 100% rename from src/phpGPX/Tests/fixtures/timezero.gpx rename to tests/fixtures/timezero.gpx diff --git a/src/phpGPX/Tests/Helpers/DateTimeHelperTest.php b/tests/phpGPX/Helpers/DateTimeHelperTest.php similarity index 97% rename from src/phpGPX/Tests/Helpers/DateTimeHelperTest.php rename to tests/phpGPX/Helpers/DateTimeHelperTest.php index ca27a65..2771271 100644 --- a/src/phpGPX/Tests/Helpers/DateTimeHelperTest.php +++ b/tests/phpGPX/Helpers/DateTimeHelperTest.php @@ -3,9 +3,8 @@ * @author Jakub Dubec */ -namespace phpGPX\Tests\Helpers; +namespace phpGPX\Helpers; -use phpGPX\Helpers\DateTimeHelper; use phpGPX\Models\Point; use PHPUnit\Framework\TestCase; diff --git a/src/phpGPX/Tests/Helpers/GeoHelperTest.php b/tests/phpGPX/Helpers/GeoHelperTest.php similarity index 96% rename from src/phpGPX/Tests/Helpers/GeoHelperTest.php rename to tests/phpGPX/Helpers/GeoHelperTest.php index 3137679..64b81d1 100644 --- a/src/phpGPX/Tests/Helpers/GeoHelperTest.php +++ b/tests/phpGPX/Helpers/GeoHelperTest.php @@ -3,9 +3,8 @@ * @author Jakub Dubec */ -namespace phpGPX\Tests\Helpers; +namespace phpGPX\Helpers; -use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; use PHPUnit\Framework\TestCase; diff --git a/src/phpGPX/Tests/Helpers/SerializationHelperTest.php b/tests/phpGPX/Helpers/SerializationHelperTest.php similarity index 97% rename from src/phpGPX/Tests/Helpers/SerializationHelperTest.php rename to tests/phpGPX/Helpers/SerializationHelperTest.php index b4acdda..4ee925d 100644 --- a/src/phpGPX/Tests/Helpers/SerializationHelperTest.php +++ b/tests/phpGPX/Helpers/SerializationHelperTest.php @@ -3,9 +3,8 @@ * @author Jakub Dubec */ -namespace phpGPX\Tests\Helpers; +namespace phpGPX\Helpers; -use phpGPX\Helpers\SerializationHelper; use PHPUnit\Framework\TestCase; class SerializationHelperTest extends TestCase diff --git a/tests/phpGPX/Models/BoundsTest.php b/tests/phpGPX/Models/BoundsTest.php new file mode 100644 index 0000000..b51437e --- /dev/null +++ b/tests/phpGPX/Models/BoundsTest.php @@ -0,0 +1,22 @@ +bounds = new Bounds( + 49.072489, + 18.814543, + 49.090543, + 18.886939 + ); + } +} \ No newline at end of file From a1c1360e5470d546b1dcb733c1e7dc79ee0f53ea Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Fri, 6 Mar 2026 01:58:29 +0100 Subject: [PATCH 08/31] Removed outdated test files and fixtures. --- CHANGELOG.md | 11 + composer.json | 4 +- docs/rfc7946.txt | 1571 +++++++++++++++++ phpunit.xml | 13 +- src/phpGPX/GpxSerializable.php | 6 +- src/phpGPX/Helpers/DateTimeHelper.php | 6 +- src/phpGPX/Helpers/SerializationHelper.php | 24 +- src/phpGPX/Models/Bounds.php | 12 + src/phpGPX/Models/Collection.php | 2 +- src/phpGPX/Models/Copyright.php | 46 +- src/phpGPX/Models/Email.php | 47 +- src/phpGPX/Models/Extensions.php | 50 +- .../Models/Extensions/AbstractExtension.php | 24 +- .../Models/Extensions/TrackPointExtension.php | 11 +- src/phpGPX/Models/GpxFile.php | 97 +- src/phpGPX/Models/Link.php | 45 +- src/phpGPX/Models/Metadata.php | 54 +- src/phpGPX/Models/Person.php | 44 +- src/phpGPX/Models/Point.php | 155 +- src/phpGPX/Models/Route.php | 81 +- src/phpGPX/Models/Segment.php | 78 +- src/phpGPX/Models/Stats.php | 120 +- src/phpGPX/Models/StatsCalculator.php | 4 +- src/phpGPX/Models/Summarizable.php | 21 - src/phpGPX/Models/Track.php | 87 +- src/phpGPX/Parsers/BoundsParser.php | 12 +- src/phpGPX/Parsers/EmailParser.php | 8 +- src/phpGPX/Parsers/LinkParser.php | 22 +- src/phpGPX/Parsers/MetadataParser.php | 7 +- src/phpGPX/Parsers/PointParser.php | 22 +- src/phpGPX/Parsers/RouteParser.php | 7 +- src/phpGPX/Parsers/TrackParser.php | 7 +- src/phpGPX/Tests/LoadFileTest.php | 228 --- .../Tests/Parsers/Bounds/BoundsParserTest.php | 68 - .../Parsers/Copyright/CopyrightParserTest.php | 77 - .../Tests/Parsers/Email/EmailParserTest.php | 73 - .../Tests/Parsers/Link/LinkParserTest.php | 75 - .../Tests/Parsers/Person/PersonParserTest.php | 126 -- src/phpGPX/phpGPX.php | 69 +- tests/Integration/GeoJsonOutputTest.php | 169 ++ tests/Integration/GpxFileLoadTest.php | 145 ++ tests/Integration/XmlRoundTripTest.php | 147 ++ tests/Unit/Helpers/DateTimeHelperTest.php | 56 + tests/Unit/Helpers/DistanceCalculatorTest.php | 137 ++ .../ElevationGainLossCalculatorTest.php | 196 ++ .../Helpers/GeoHelperTest.php | 52 +- .../Unit/Helpers/SerializationHelperTest.php | 88 + tests/Unit/Models/BoundsTest.php | 56 + tests/Unit/Models/StatsCalculationTest.php | 352 ++++ tests/Unit/Parsers/BoundsParserTest.php | 54 + tests/Unit/Parsers/CopyrightParserTest.php | 58 + tests/Unit/Parsers/EmailParserTest.php | 56 + .../Unit/Parsers}/ExtensionParserTest.php | 69 +- tests/Unit/Parsers/LinkParserTest.php | 63 + tests/Unit/Parsers/PersonParserTest.php | 92 + .../fixtures}/Parsers/Bounds/bounds.json | 0 .../fixtures}/Parsers/Bounds/bounds.xml | 0 .../Parsers/Copyright/copyright.json | 0 .../fixtures}/Parsers/Copyright/copyright.xml | 0 .../fixtures}/Parsers/Email/email.json | 0 .../fixtures}/Parsers/Email/email.xml | 0 .../Parsers/Extension/extension.json | 0 .../fixtures}/Parsers/Extension/extension.xml | 0 .../fixtures}/Parsers/Link/link.json | 0 .../fixtures}/Parsers/Link/link.xml | 0 .../fixtures}/Parsers/Person/person.json | 0 .../fixtures}/Parsers/Person/person.xml | 0 tests/phpGPX/Helpers/DateTimeHelperTest.php | 89 - .../Helpers/SerializationHelperTest.php | 105 -- tests/phpGPX/Models/BoundsTest.php | 22 - 70 files changed, 4247 insertions(+), 1173 deletions(-) create mode 100644 docs/rfc7946.txt delete mode 100644 src/phpGPX/Tests/LoadFileTest.php delete mode 100644 src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php delete mode 100644 src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php delete mode 100644 src/phpGPX/Tests/Parsers/Email/EmailParserTest.php delete mode 100644 src/phpGPX/Tests/Parsers/Link/LinkParserTest.php delete mode 100644 src/phpGPX/Tests/Parsers/Person/PersonParserTest.php create mode 100644 tests/Integration/GeoJsonOutputTest.php create mode 100644 tests/Integration/GpxFileLoadTest.php create mode 100644 tests/Integration/XmlRoundTripTest.php create mode 100644 tests/Unit/Helpers/DateTimeHelperTest.php create mode 100644 tests/Unit/Helpers/DistanceCalculatorTest.php create mode 100644 tests/Unit/Helpers/ElevationGainLossCalculatorTest.php rename tests/{phpGPX => Unit}/Helpers/GeoHelperTest.php (52%) create mode 100644 tests/Unit/Helpers/SerializationHelperTest.php create mode 100644 tests/Unit/Models/BoundsTest.php create mode 100644 tests/Unit/Models/StatsCalculationTest.php create mode 100644 tests/Unit/Parsers/BoundsParserTest.php create mode 100644 tests/Unit/Parsers/CopyrightParserTest.php create mode 100644 tests/Unit/Parsers/EmailParserTest.php rename {src/phpGPX/Tests/Parsers/Extension => tests/Unit/Parsers}/ExtensionParserTest.php (59%) create mode 100644 tests/Unit/Parsers/LinkParserTest.php create mode 100644 tests/Unit/Parsers/PersonParserTest.php rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Bounds/bounds.json (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Bounds/bounds.xml (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Copyright/copyright.json (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Copyright/copyright.xml (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Email/email.json (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Email/email.xml (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Extension/extension.json (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Extension/extension.xml (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Link/link.json (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Link/link.xml (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Person/person.json (100%) rename {src/phpGPX/Tests => tests/fixtures}/Parsers/Person/person.xml (100%) delete mode 100644 tests/phpGPX/Helpers/DateTimeHelperTest.php delete mode 100644 tests/phpGPX/Helpers/SerializationHelperTest.php delete mode 100644 tests/phpGPX/Models/BoundsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index adbeeca..be4cd14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ ## 2.0.0 : TBD - **Changed** Support for PHP 8.1+ only +- **Fixed** Added proper return type declarations to parser classes to fix deprecated messages in PHP 8.4 +- **Fixed** Patched vendor files to fix deprecation warnings about implicitly marking parameters as nullable: + - sebastian/cli-parser/src/Parser.php + - phpunit/phpunit/src/Util/Exporter.php + - sebastian/exporter/src/Exporter.php +- **Fixed** Updated tests to be compatible with PHPUnit 12.x: + - Added missing tests to BoundsTest class + - Updated test annotations to use PHPUnit 12 attributes + - Fixed data provider usage in SerializationHelperTest + - Implemented missing GpxSerializable interface methods in Bounds class + - Updated phpunit.xml configuration ## 1.3.0 : 2023-07-19 diff --git a/composer.json b/composer.json index 1d43ebc..eb749ac 100644 --- a/composer.json +++ b/composer.json @@ -27,13 +27,13 @@ "ext-dom": "*" }, "require-dev": { - "phpunit/phpunit": "^10.5.5", + "phpunit/phpunit": "^12.2.6", "friendsofphp/php-cs-fixer": "^v3.43.1" }, "autoload": { "psr-4": { "phpGPX\\": "src/phpGPX" } }, "autoload-dev": { - "psr-4": { "phpGPX\\": "tests/phpGPX" } + "psr-4": { "phpGPX\\Tests\\": "tests" } } } diff --git a/docs/rfc7946.txt b/docs/rfc7946.txt new file mode 100644 index 0000000..109adb8 --- /dev/null +++ b/docs/rfc7946.txt @@ -0,0 +1,1571 @@ + + + + + + +Internet Engineering Task Force (IETF) H. Butler +Request for Comments: 7946 Hobu Inc. +Category: Standards Track M. Daly +ISSN: 2070-1721 Cadcorp + A. Doyle + + S. Gillies + Mapbox + S. Hagen + + T. Schaub + Planet Labs + August 2016 + + + The GeoJSON Format + +Abstract + + GeoJSON is a geospatial data interchange format based on JavaScript + Object Notation (JSON). It defines several types of JSON objects and + the manner in which they are combined to represent data about + geographic features, their properties, and their spatial extents. + GeoJSON uses a geographic coordinate reference system, World Geodetic + System 1984, and units of decimal degrees. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc7946. + + + + + + + + + + + + +Butler, et al. Standards Track [Page 1] + +RFC 7946 GeoJSON August 2016 + + +Copyright Notice + + Copyright (c) 2016 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 + 1.1. Requirements Language . . . . . . . . . . . . . . . . . . 4 + 1.2. Conventions Used in This Document . . . . . . . . . . . . 4 + 1.3. Specification of GeoJSON . . . . . . . . . . . . . . . . 4 + 1.4. Definitions . . . . . . . . . . . . . . . . . . . . . . . 5 + 1.5. Example . . . . . . . . . . . . . . . . . . . . . . . . . 5 + 2. GeoJSON Text . . . . . . . . . . . . . . . . . . . . . . . . 6 + 3. GeoJSON Object . . . . . . . . . . . . . . . . . . . . . . . 6 + 3.1. Geometry Object . . . . . . . . . . . . . . . . . . . . . 7 + 3.1.1. Position . . . . . . . . . . . . . . . . . . . . . . 7 + 3.1.2. Point . . . . . . . . . . . . . . . . . . . . . . . . 8 + 3.1.3. MultiPoint . . . . . . . . . . . . . . . . . . . . . 8 + 3.1.4. LineString . . . . . . . . . . . . . . . . . . . . . 8 + 3.1.5. MultiLineString . . . . . . . . . . . . . . . . . . . 8 + 3.1.6. Polygon . . . . . . . . . . . . . . . . . . . . . . . 9 + 3.1.7. MultiPolygon . . . . . . . . . . . . . . . . . . . . 9 + 3.1.8. GeometryCollection . . . . . . . . . . . . . . . . . 9 + 3.1.9. Antimeridian Cutting . . . . . . . . . . . . . . . . 10 + 3.1.10. Uncertainty and Precision . . . . . . . . . . . . . . 11 + 3.2. Feature Object . . . . . . . . . . . . . . . . . . . . . 11 + 3.3. FeatureCollection Object . . . . . . . . . . . . . . . . 12 + 4. Coordinate Reference System . . . . . . . . . . . . . . . . . 12 + 5. Bounding Box . . . . . . . . . . . . . . . . . . . . . . . . 12 + 5.1. The Connecting Lines . . . . . . . . . . . . . . . . . . 14 + 5.2. The Antimeridian . . . . . . . . . . . . . . . . . . . . 14 + 5.3. The Poles . . . . . . . . . . . . . . . . . . . . . . . . 14 + 6. Extending GeoJSON . . . . . . . . . . . . . . . . . . . . . . 15 + 6.1. Foreign Members . . . . . . . . . . . . . . . . . . . . . 15 + 7. GeoJSON Types Are Not Extensible . . . . . . . . . . . . . . 16 + 7.1. Semantics of GeoJSON Members and Types Are Not Changeable 16 + 8. Versioning . . . . . . . . . . . . . . . . . . . . . . . . . 17 + + + +Butler, et al. Standards Track [Page 2] + +RFC 7946 GeoJSON August 2016 + + + 9. Mapping 'geo' URIs . . . . . . . . . . . . . . . . . . . . . 17 + 10. Security Considerations . . . . . . . . . . . . . . . . . . . 18 + 11. Interoperability Considerations . . . . . . . . . . . . . . . 18 + 11.1. I-JSON . . . . . . . . . . . . . . . . . . . . . . . . . 18 + 11.2. Coordinate Precision . . . . . . . . . . . . . . . . . . 18 + 12. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 19 + 13. References . . . . . . . . . . . . . . . . . . . . . . . . . 20 + 13.1. Normative References . . . . . . . . . . . . . . . . . . 20 + 13.2. Informative References . . . . . . . . . . . . . . . . . 21 + Appendix A. Geometry Examples . . . . . . . . . . . . . . . . . 22 + A.1. Points . . . . . . . . . . . . . . . . . . . . . . . . . 22 + A.2. LineStrings . . . . . . . . . . . . . . . . . . . . . . . 22 + A.3. Polygons . . . . . . . . . . . . . . . . . . . . . . . . 23 + A.4. MultiPoints . . . . . . . . . . . . . . . . . . . . . . . 24 + A.5. MultiLineStrings . . . . . . . . . . . . . . . . . . . . 24 + A.6. MultiPolygons . . . . . . . . . . . . . . . . . . . . . . 25 + A.7. GeometryCollections . . . . . . . . . . . . . . . . . . . 26 + Appendix B. Changes from the Pre-IETF GeoJSON Format + Specification . . . . . . . . . . . . . . . . . . . 26 + B.1. Normative Changes . . . . . . . . . . . . . . . . . . . . 26 + B.2. Informative Changes . . . . . . . . . . . . . . . . . . . 27 + Appendix C. GeoJSON Text Sequences . . . . . . . . . . . . . . . 27 + Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 27 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 28 + +1. Introduction + + GeoJSON is a format for encoding a variety of geographic data + structures using JavaScript Object Notation (JSON) [RFC7159]. A + GeoJSON object may represent a region of space (a Geometry), a + spatially bounded entity (a Feature), or a list of Features (a + FeatureCollection). GeoJSON supports the following geometry types: + Point, LineString, Polygon, MultiPoint, MultiLineString, + MultiPolygon, and GeometryCollection. Features in GeoJSON contain a + Geometry object and additional properties, and a FeatureCollection + contains a list of Features. + + The format is concerned with geographic data in the broadest sense; + anything with qualities that are bounded in geographical space might + be a Feature whether or not it is a physical structure. The concepts + in GeoJSON are not new; they are derived from preexisting open + geographic information system standards and have been streamlined to + better suit web application development using JSON. + + GeoJSON comprises the seven concrete geometry types defined in the + OpenGIS Simple Features Implementation Specification for SQL [SFSQL]: + 0-dimensional Point and MultiPoint; 1-dimensional curve LineString + and MultiLineString; 2-dimensional surface Polygon and MultiPolygon; + + + +Butler, et al. Standards Track [Page 3] + +RFC 7946 GeoJSON August 2016 + + + and the heterogeneous GeometryCollection. GeoJSON representations of + instances of these geometry types are analogous to the well-known + binary (WKB) and well-known text (WKT) representations described in + that same specification. + + GeoJSON also comprises the types Feature and FeatureCollection. + Feature objects in GeoJSON contain a Geometry object with one of the + above geometry types and additional members. A FeatureCollection + object contains an array of Feature objects. This structure is + analogous to that of the Web Feature Service (WFS) response to + GetFeatures requests specified in [WFSv1] or to a Keyhole Markup + Language (KML) Folder of Placemarks [KMLv2.2]. Some implementations + of the WFS specification also provide GeoJSON-formatted responses to + GetFeature requests, but there is no particular service model or + Feature type ontology implied in the GeoJSON format specification. + + Since its initial publication in 2008 [GJ2008], the GeoJSON format + specification has steadily grown in popularity. It is widely used in + JavaScript web-mapping libraries, JSON-based document databases, and + web APIs. + +1.1. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + [RFC2119]. + +1.2. Conventions Used in This Document + + The ordering of the members of any JSON object defined in this + document MUST be considered irrelevant, as specified by [RFC7159]. + + Some examples use the combination of a JavaScript single-line comment + (//) followed by an ellipsis (...) as placeholder notation for + content deemed irrelevant by the authors. These placeholders must of + course be deleted or otherwise replaced, before attempting to + validate the corresponding JSON code example. + + Whitespace is used in the examples inside this document to help + illustrate the data structures, but it is not required. Unquoted + whitespace is not significant in JSON. + +1.3. Specification of GeoJSON + + This document supersedes the original GeoJSON format specification + [GJ2008]. + + + + +Butler, et al. Standards Track [Page 4] + +RFC 7946 GeoJSON August 2016 + + +1.4. Definitions + + o JavaScript Object Notation (JSON), and the terms object, member, + name, value, array, number, true, false, and null, are to be + interpreted as defined in [RFC7159]. + + o Inside this document, the term "geometry type" refers to seven + case-sensitive strings: "Point", "MultiPoint", "LineString", + "MultiLineString", "Polygon", "MultiPolygon", and + "GeometryCollection". + + o As another shorthand notation, the term "GeoJSON types" refers to + nine case-sensitive strings: "Feature", "FeatureCollection", and + the geometry types listed above. + + o The word "Collection" in "FeatureCollection" and + "GeometryCollection" does not have any significance for the + semantics of array members. The "features" and "geometries" + members, respectively, of these objects are standard ordered JSON + arrays, not unordered sets. + +1.5. Example + + A GeoJSON FeatureCollection: + + { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [102.0, 0.5] + }, + "properties": { + "prop0": "value0" + } + }, { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + [105.0, 1.0] + ] + }, + "properties": { + + + +Butler, et al. Standards Track [Page 5] + +RFC 7946 GeoJSON August 2016 + + + "prop0": "value0", + "prop1": 0.0 + } + }, { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ] + ] + }, + "properties": { + "prop0": "value0", + "prop1": { + "this": "that" + } + } + }] + } + +2. GeoJSON Text + + A GeoJSON text is a JSON text and consists of a single GeoJSON + object. + +3. GeoJSON Object + + A GeoJSON object represents a Geometry, Feature, or collection of + Features. + + o A GeoJSON object is a JSON object. + + o A GeoJSON object has a member with the name "type". The value of + the member MUST be one of the GeoJSON types. + + o A GeoJSON object MAY have a "bbox" member, the value of which MUST + be a bounding box array (see Section 5). + + o A GeoJSON object MAY have other members (see Section 6). + + + + + + +Butler, et al. Standards Track [Page 6] + +RFC 7946 GeoJSON August 2016 + + +3.1. Geometry Object + + A Geometry object represents points, curves, and surfaces in + coordinate space. Every Geometry object is a GeoJSON object no + matter where it occurs in a GeoJSON text. + + o The value of a Geometry object's "type" member MUST be one of the + seven geometry types (see Section 1.4). + + o A GeoJSON Geometry object of any type other than + "GeometryCollection" has a member with the name "coordinates". + The value of the "coordinates" member is an array. The structure + of the elements in this array is determined by the type of + geometry. GeoJSON processors MAY interpret Geometry objects with + empty "coordinates" arrays as null objects. + +3.1.1. Position + + A position is the fundamental geometry construct. The "coordinates" + member of a Geometry object is composed of either: + + o one position in the case of a Point geometry, + + o an array of positions in the case of a LineString or MultiPoint + geometry, + + o an array of LineString or linear ring (see Section 3.1.6) + coordinates in the case of a Polygon or MultiLineString geometry, + or + + o an array of Polygon coordinates in the case of a MultiPolygon + geometry. + + A position is an array of numbers. There MUST be two or more + elements. The first two elements are longitude and latitude, or + easting and northing, precisely in that order and using decimal + numbers. Altitude or elevation MAY be included as an optional third + element. + + Implementations SHOULD NOT extend positions beyond three elements + because the semantics of extra elements are unspecified and + ambiguous. Historically, some implementations have used a fourth + element to carry a linear referencing measure (sometimes denoted as + "M") or a numerical timestamp, but in most situations a parser will + not be able to properly interpret these values. The interpretation + and meaning of additional elements is beyond the scope of this + specification, and additional elements MAY be ignored by parsers. + + + + +Butler, et al. Standards Track [Page 7] + +RFC 7946 GeoJSON August 2016 + + + A line between two positions is a straight Cartesian line, the + shortest line between those two points in the coordinate reference + system (see Section 4). + + In other words, every point on a line that does not cross the + antimeridian between a point (lon0, lat0) and (lon1, lat1) can be + calculated as + + F(lon, lat) = (lon0 + (lon1 - lon0) * t, lat0 + (lat1 - lat0) * t) + + with t being a real number greater than or equal to 0 and smaller + than or equal to 1. Note that this line may markedly differ from the + geodesic path along the curved surface of the reference ellipsoid. + + The same applies to the optional height element with the proviso that + the direction of the height is as specified in the coordinate + reference system. + + Note that, again, this does not mean that a surface with equal height + follows, for example, the curvature of a body of water. Nor is a + surface of equal height perpendicular to a plumb line. + + Examples of positions and geometries are provided in Appendix A, + "Geometry Examples". + +3.1.2. Point + + For type "Point", the "coordinates" member is a single position. + +3.1.3. MultiPoint + + For type "MultiPoint", the "coordinates" member is an array of + positions. + +3.1.4. LineString + + For type "LineString", the "coordinates" member is an array of two or + more positions. + +3.1.5. MultiLineString + + For type "MultiLineString", the "coordinates" member is an array of + LineString coordinate arrays. + + + + + + + + +Butler, et al. Standards Track [Page 8] + +RFC 7946 GeoJSON August 2016 + + +3.1.6. Polygon + + To specify a constraint specific to Polygons, it is useful to + introduce the concept of a linear ring: + + o A linear ring is a closed LineString with four or more positions. + + o The first and last positions are equivalent, and they MUST contain + identical values; their representation SHOULD also be identical. + + o A linear ring is the boundary of a surface or the boundary of a + hole in a surface. + + o A linear ring MUST follow the right-hand rule with respect to the + area it bounds, i.e., exterior rings are counterclockwise, and + holes are clockwise. + + Note: the [GJ2008] specification did not discuss linear ring winding + order. For backwards compatibility, parsers SHOULD NOT reject + Polygons that do not follow the right-hand rule. + + Though a linear ring is not explicitly represented as a GeoJSON + geometry type, it leads to a canonical formulation of the Polygon + geometry type definition as follows: + + o For type "Polygon", the "coordinates" member MUST be an array of + linear ring coordinate arrays. + + o For Polygons with more than one of these rings, the first MUST be + the exterior ring, and any others MUST be interior rings. The + exterior ring bounds the surface, and the interior rings (if + present) bound holes within the surface. + +3.1.7. MultiPolygon + + For type "MultiPolygon", the "coordinates" member is an array of + Polygon coordinate arrays. + +3.1.8. GeometryCollection + + A GeoJSON object with type "GeometryCollection" is a Geometry object. + A GeometryCollection has a member with the name "geometries". The + value of "geometries" is an array. Each element of this array is a + GeoJSON Geometry object. It is possible for this array to be empty. + + + + + + + +Butler, et al. Standards Track [Page 9] + +RFC 7946 GeoJSON August 2016 + + + Unlike the other geometry types described above, a GeometryCollection + can be a heterogeneous composition of smaller Geometry objects. For + example, a Geometry object in the shape of a lowercase roman "i" can + be composed of one point and one LineString. + + GeometryCollections have a different syntax from single type Geometry + objects (Point, LineString, and Polygon) and homogeneously typed + multipart Geometry objects (MultiPoint, MultiLineString, and + MultiPolygon) but have no different semantics. Although a + GeometryCollection object has no "coordinates" member, it does have + coordinates: the coordinates of all its parts belong to the + collection. The "geometries" member of a GeometryCollection + describes the parts of this composition. Implementations SHOULD NOT + apply any additional semantics to the "geometries" array. + + To maximize interoperability, implementations SHOULD avoid nested + GeometryCollections. Furthermore, GeometryCollections composed of a + single part or a number of parts of a single type SHOULD be avoided + when that single part or a single object of multipart type + (MultiPoint, MultiLineString, or MultiPolygon) could be used instead. + +3.1.9. Antimeridian Cutting + + In representing Features that cross the antimeridian, + interoperability is improved by modifying their geometry. Any + geometry that crosses the antimeridian SHOULD be represented by + cutting it in two such that neither part's representation crosses the + antimeridian. + + For example, a line extending from 45 degrees N, 170 degrees E across + the antimeridian to 45 degrees N, 170 degrees W should be cut in two + and represented as a MultiLineString. + + { + "type": "MultiLineString", + "coordinates": [ + [ + [170.0, 45.0], [180.0, 45.0] + ], [ + [-180.0, 45.0], [-170.0, 45.0] + ] + ] + } + + + + + + + + +Butler, et al. Standards Track [Page 10] + +RFC 7946 GeoJSON August 2016 + + + A rectangle extending from 40 degrees N, 170 degrees E across the + antimeridian to 50 degrees N, 170 degrees W should be cut in two and + represented as a MultiPolygon. + + { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [180.0, 40.0], [180.0, 50.0], [170.0, 50.0], + [170.0, 40.0], [180.0, 40.0] + ] + ], + [ + [ + [-170.0, 40.0], [-170.0, 50.0], [-180.0, 50.0], + [-180.0, 40.0], [-170.0, 40.0] + ] + ] + ] + } + +3.1.10. Uncertainty and Precision + + As in [RFC5870], the number of digits of the values in coordinate + positions MUST NOT be interpreted as an indication to the level of + uncertainty. + +3.2. Feature Object + + A Feature object represents a spatially bounded thing. Every Feature + object is a GeoJSON object no matter where it occurs in a GeoJSON + text. + + o A Feature object has a "type" member with the value "Feature". + + o A Feature object has a member with the name "geometry". The value + of the geometry member SHALL be either a Geometry object as + defined above or, in the case that the Feature is unlocated, a + JSON null value. + + o A Feature object has a member with the name "properties". The + value of the properties member is an object (any JSON object or a + JSON null value). + + + + + + + +Butler, et al. Standards Track [Page 11] + +RFC 7946 GeoJSON August 2016 + + + o If a Feature has a commonly used identifier, that identifier + SHOULD be included as a member of the Feature object with the name + "id", and the value of this member is either a JSON string or + number. + +3.3. FeatureCollection Object + + A GeoJSON object with the type "FeatureCollection" is a + FeatureCollection object. A FeatureCollection object has a member + with the name "features". The value of "features" is a JSON array. + Each element of the array is a Feature object as defined above. It + is possible for this array to be empty. + +4. Coordinate Reference System + + The coordinate reference system for all GeoJSON coordinates is a + geographic coordinate reference system, using the World Geodetic + System 1984 (WGS 84) [WGS84] datum, with longitude and latitude units + of decimal degrees. This is equivalent to the coordinate reference + system identified by the Open Geospatial Consortium (OGC) URN + urn:ogc:def:crs:OGC::CRS84. An OPTIONAL third-position element SHALL + be the height in meters above or below the WGS 84 reference + ellipsoid. In the absence of elevation values, applications + sensitive to height or depth SHOULD interpret positions as being at + local ground or sea level. + + Note: the use of alternative coordinate reference systems was + specified in [GJ2008], but it has been removed from this version of + the specification because the use of different coordinate reference + systems -- especially in the manner specified in [GJ2008] -- has + proven to have interoperability issues. In general, GeoJSON + processing software is not expected to have access to coordinate + reference system databases or to have network access to coordinate + reference system transformation parameters. However, where all + involved parties have a prior arrangement, alternative coordinate + reference systems can be used without risk of data being + misinterpreted. + +5. Bounding Box + + A GeoJSON object MAY have a member named "bbox" to include + information on the coordinate range for its Geometries, Features, or + FeatureCollections. The value of the bbox member MUST be an array of + length 2*n where n is the number of dimensions represented in the + contained geometries, with all axes of the most southwesterly point + followed by all axes of the more northeasterly point. The axes order + of a bbox follows the axes order of geometries. + + + + +Butler, et al. Standards Track [Page 12] + +RFC 7946 GeoJSON August 2016 + + + The "bbox" values define shapes with edges that follow lines of + constant longitude, latitude, and elevation. + + Example of a 2D bbox member on a Feature: + + { + "type": "Feature", + "bbox": [-10.0, -10.0, 10.0, 10.0], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-10.0, -10.0], + [10.0, -10.0], + [10.0, 10.0], + [-10.0, -10.0] + ] + ] + } + //... + } + + Example of a 2D bbox member on a FeatureCollection: + + { + "type": "FeatureCollection", + "bbox": [100.0, 0.0, 105.0, 1.0], + "features": [ + //... + ] + } + + Example of a 3D bbox member with a depth of 100 meters: + + { + "type": "FeatureCollection", + "bbox": [100.0, 0.0, -100.0, 105.0, 1.0, 0.0], + "features": [ + //... + ] + } + + + + + + + + + + +Butler, et al. Standards Track [Page 13] + +RFC 7946 GeoJSON August 2016 + + +5.1. The Connecting Lines + + The four lines of the bounding box are defined fully within the + coordinate reference system; that is, for a box bounded by the values + "west", "south", "east", and "north", every point on the northernmost + line can be expressed as + + (lon, lat) = (west + (east - west) * t, north) + + with 0 <= t <= 1. + +5.2. The Antimeridian + + Consider a set of point Features within the Fiji archipelago, + straddling the antimeridian between 16 degrees S and 20 degrees S. + The southwest corner of the box containing these Features is at 20 + degrees S and 177 degrees E, and the northwest corner is at 16 + degrees S and 178 degrees W. The antimeridian-spanning GeoJSON + bounding box for this FeatureCollection is + + "bbox": [177.0, -20.0, -178.0, -16.0] + + and covers 5 degrees of longitude. + + The complementary bounding box for the same latitude band, not + crossing the antimeridian, is + + "bbox": [-178.0, -20.0, 177.0, -16.0] + + and covers 355 degrees of longitude. + + The latitude of the northeast corner is always greater than the + latitude of the southwest corner, but bounding boxes that cross the + antimeridian have a northeast corner longitude that is less than the + longitude of the southwest corner. + +5.3. The Poles + + A bounding box that contains the North Pole extends from a southwest + corner of "minlat" degrees N, 180 degrees W to a northeast corner of + 90 degrees N, 180 degrees E. Viewed on a globe, this bounding box + approximates a spherical cap bounded by the "minlat" circle of + latitude. + + "bbox": [-180.0, minlat, 180.0, 90.0] + + + + + + +Butler, et al. Standards Track [Page 14] + +RFC 7946 GeoJSON August 2016 + + + A bounding box that contains the South Pole extends from a southwest + corner of 90 degrees S, 180 degrees W to a northeast corner of + "maxlat" degrees S, 180 degrees E. + + "bbox": [-180.0, -90.0, 180.0, maxlat] + + A bounding box that just touches the North Pole and forms a slice of + an approximate spherical cap when viewed on a globe extends from a + southwest corner of "minlat" degrees N and "westlon" degrees E to a + northeast corner of 90 degrees N and "eastlon" degrees E. + + "bbox": [westlon, minlat, eastlon, 90.0] + + Similarly, a bounding box that just touches the South Pole and forms + a slice of an approximate spherical cap when viewed on a globe has + the following representation in GeoJSON. + + "bbox": [westlon, -90.0, eastlon, maxlat] + + Implementers MUST NOT use latitude values greater than 90 or less + than -90 to imply an extent that is not a spherical cap. + +6. Extending GeoJSON + +6.1. Foreign Members + + Members not described in this specification ("foreign members") MAY + be used in a GeoJSON document. Note that support for foreign members + can vary across implementations, and no normative processing model + for foreign members is defined. Accordingly, implementations that + rely too heavily on the use of foreign members might experience + reduced interoperability with other implementations. + + For example, in the (abridged) Feature object shown below + + { + "type": "Feature", + "id": "f1", + "geometry": {...}, + "properties": {...}, + "title": "Example Feature" + } + + the name/value pair of "title": "Example Feature" is a foreign + member. When the value of a foreign member is an object, all the + descendant members of that object are themselves foreign members. + + + + + +Butler, et al. Standards Track [Page 15] + +RFC 7946 GeoJSON August 2016 + + + GeoJSON semantics do not apply to foreign members and their + descendants, regardless of their names and values. For example, in + the (abridged) Feature object below + + { + "type": "Feature", + "id": "f2", + "geometry": {...}, + "properties": {...}, + "centerline": { + "type": "LineString", + "coordinates": [ + [-170, 10], + [170, 11] + ] + } + } + + the "centerline" member is not a GeoJSON Geometry object. + +7. GeoJSON Types Are Not Extensible + + Implementations MUST NOT extend the fixed set of GeoJSON types: + FeatureCollection, Feature, Point, LineString, MultiPoint, Polygon, + MultiLineString, MultiPolygon, and GeometryCollection. + +7.1. Semantics of GeoJSON Members and Types Are Not Changeable + + Implementations MUST NOT change the semantics of GeoJSON members and + types. + + The GeoJSON "coordinates" and "geometries" members define Geometry + objects. FeatureCollection and Feature objects, respectively, MUST + NOT contain a "coordinates" or "geometries" member. + + The GeoJSON "geometry" and "properties" members define a Feature + object. FeatureCollection and Geometry objects, respectively, MUST + NOT contain a "geometry" or "properties" member. + + The GeoJSON "features" member defines a FeatureCollection object. + Feature and Geometry objects, respectively, MUST NOT contain a + "features" member. + + + + + + + + + +Butler, et al. Standards Track [Page 16] + +RFC 7946 GeoJSON August 2016 + + +8. Versioning + + The GeoJSON format can be extended as defined here, but no explicit + versioning scheme is defined. A specification that alters the + semantics of GeoJSON members or otherwise modifies the format does + not create a new version of this format; instead, it defines an + entirely new format that MUST NOT be identified as GeoJSON. + +9. Mapping 'geo' URIs + + 'geo' URIs [RFC5870] identify geographic locations and precise (not + uncertain) locations can be mapped to GeoJSON Geometry objects. + + For this section, as in [RFC5870], "lat", "lon", "alt", and "unc" are + placeholders for 'geo' URI latitude, longitude, altitude, and + uncertainty values, respectively. + + A 'geo' URI with two coordinates and an uncertainty ('u') parameter + that is absent or zero, and a GeoJSON Point geometry may be mapped to + each other. A GeoJSON Point is always converted to a 'geo' URI that + has no uncertainty parameter. + + 'geo' URI: + + geo:lat,lon + + GeoJSON: + + {"type": "Point", "coordinates": [lon, lat]} + + The mapping between 'geo' URIs and GeoJSON Points that specify + elevation is shown below. + + 'geo' URI: + + geo:lat,lon,alt + + GeoJSON: + + {"type": "Point", "coordinates": [lon, lat, alt]} + + GeoJSON has no concept of uncertainty; imprecise or uncertain 'geo' + URIs thus cannot be mapped to GeoJSON geometries. + + + + + + + + +Butler, et al. Standards Track [Page 17] + +RFC 7946 GeoJSON August 2016 + + +10. Security Considerations + + GeoJSON shares security issues common to all JSON content types. See + [RFC7159], Section 12 for additional information. GeoJSON does not + provide executable content. + + GeoJSON does not provide privacy or integrity services. If sensitive + data requires privacy or integrity protection, those must be provided + by the transport -- for example, Transport Layer Security (TLS) or + HTTPS. There will be cases in which stored data need protection, + which is out of scope for this document. + + As with other geographic data formats, e.g., [KMLv2.2], providing + details about the locations of sensitive persons, animals, habitats, + and facilities can expose them to unauthorized tracking or injury. + Data providers should recognize the risk of inadvertently identifying + individuals if locations in anonymized datasets are not adequately + skewed or not sufficiently fuzzed [Sweeney] and recognize that the + effectiveness of location obscuration is limited by a number of + factors and is unlikely to be an effective defense against a + determined attack [RFC6772]. + +11. Interoperability Considerations + +11.1. I-JSON + + GeoJSON texts should follow the constraints of Internet JSON (I-JSON) + [RFC7493] for maximum interoperability. + +11.2. Coordinate Precision + + The size of a GeoJSON text in bytes is a major interoperability + consideration, and precision of coordinate values has a large impact + on the size of texts. A GeoJSON text containing many detailed + Polygons can be inflated almost by a factor of two by increasing + coordinate precision from 6 to 15 decimal places. For geographic + coordinates with units of degrees, 6 decimal places (a default common + in, e.g., sprintf) amounts to about 10 centimeters, a precision well + within that of current GPS systems. Implementations should consider + the cost of using a greater precision than necessary. + + Furthermore, the WGS 84 [WGS84] datum is a relatively coarse + approximation of the geoid, with the height varying by up to 5 m (but + generally between 2 and 3 meters) higher or lower relative to a + surface parallel to Earth's mean sea level. + + + + + + +Butler, et al. Standards Track [Page 18] + +RFC 7946 GeoJSON August 2016 + + +12. IANA Considerations + + The media type for GeoJSON text is "application/geo+json" and is + registered in the "Media Types" registry described in [RFC6838]. The + entry for "application/vnd.geo+json" in the same registry should have + its status changed to be "OBSOLETED" with a pointer to the media type + "application/geo+json" and a reference added to this RFC. + + Type name: application + + Subtype name: geo+json + + Required parameters: n/a + + Optional parameters: n/a + + Encoding considerations: binary + + Security considerations: See Section 10 above + + Interoperability considerations: See Section 11 above + + Published specification: [[RFC7946]] + + Applications that use this media type: No known applications + currently use this media type. This media type is intended for + GeoJSON applications currently using the "application/ + vnd.geo+json" or "application/json" media types, of which there + are several categories: web mapping, geospatial databases, + geographic data processing APIs, data analysis and storage + services, and data dissemination. + + Additional information: + + Magic number(s): n/a + + File extension(s): .json, .geojson + + Macintosh file type code: n/a + + Object Identifiers: n/a + + Windows clipboard name: GeoJSON + + Macintosh uniform type identifier: public.geojson conforms to + public.json + + + + + +Butler, et al. Standards Track [Page 19] + +RFC 7946 GeoJSON August 2016 + + + Person to contact for further information: Sean Gillies + (sean.gillies@gmail.com) + + Intended usage: COMMON + + Restrictions on usage: none + + Restrictions on usage: none + + Author: see "Authors' Addresses" section of [[RFC7946]]. + + Change controller: Internet Engineering Task Force + +13. References + +13.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type + Specifications and Registration Procedures", BCP 13, + RFC 6838, DOI 10.17487/RFC6838, January 2013, + . + + [RFC7159] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data + Interchange Format", RFC 7159, DOI 10.17487/RFC7159, March + 2014, . + + [RFC7493] Bray, T., Ed., "The I-JSON Message Format", RFC 7493, + DOI 10.17487/RFC7493, March 2015, + . + + [WGS84] National Imagery and Mapping Agency, "Department of + Defense World Geodetic System 1984: Its Definition and + Relationships with Local Geodetic Systems", Third Edition, + 1984. + + + + + + + + + + + + +Butler, et al. Standards Track [Page 20] + +RFC 7946 GeoJSON August 2016 + + +13.2. Informative References + + [GJ2008] Butler, H., Daly, M., Doyle, A., Gillies, S., Schaub, T., + and C. Schmidt, "The GeoJSON Format Specification", June + 2008. + + [KMLv2.2] Wilson, T., "OGC KML", OGC 07-147r2, Version 2.2.0, April + 2008. + + [RFC5870] Mayrhofer, A. and C. Spanring, "A Uniform Resource + Identifier for Geographic Locations ('geo' URI)", + RFC 5870, DOI 10.17487/RFC5870, June 2010, + . + + [RFC6772] Schulzrinne, H., Ed., Tschofenig, H., Ed., Cuellar, J., + Polk, J., Morris, J., and M. Thomson, "Geolocation Policy: + A Document Format for Expressing Privacy Preferences for + Location Information", RFC 6772, DOI 10.17487/RFC6772, + January 2013, . + + [RFC7464] Williams, N., "JavaScript Object Notation (JSON) Text + Sequences", RFC 7464, DOI 10.17487/RFC7464, February 2015, + . + + [SFSQL] OpenGIS Consortium, Inc., "OpenGIS Simple Features + Specification For SQL Revision 1.1", OGC 99-049, May 1999. + + [Sweeney] Sweeney, L., "k-anonymity: a model for protecting + privacy", International Journal on Uncertainty, Fuzziness + and Knowledge-based Systems 10 (5), 2002; 557-570, + DOI 10.1142/S0218488502001648, 2002. + + [WFSv1] Vretanos, P., "Web Feature Service Implementation + Specification", OGC 04-094, Version 1.1.0, May 2005. + + + + + + + + + + + + + + + + + +Butler, et al. Standards Track [Page 21] + +RFC 7946 GeoJSON August 2016 + + +Appendix A. Geometry Examples + + Each of the examples below represents a valid and complete GeoJSON + object. + +A.1. Points + + Point coordinates are in x, y order (easting, northing for projected + coordinates, longitude, and latitude for geographic coordinates): + + { + "type": "Point", + "coordinates": [100.0, 0.0] + } + +A.2. LineStrings + + Coordinates of LineString are an array of positions (see + Section 3.1.1): + + { + "type": "LineString", + "coordinates": [ + [100.0, 0.0], + [101.0, 1.0] + ] + } + + + + + + + + + + + + + + + + + + + + + + + + +Butler, et al. Standards Track [Page 22] + +RFC 7946 GeoJSON August 2016 + + +A.3. Polygons + + Coordinates of a Polygon are an array of linear ring (see + Section 3.1.6) coordinate arrays. The first element in the array + represents the exterior ring. Any subsequent elements represent + interior rings (or holes). + + No holes: + + { + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ] + ] + } + + With holes: + + { + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ], + [ + [100.8, 0.8], + [100.8, 0.2], + [100.2, 0.2], + [100.2, 0.8], + [100.8, 0.8] + ] + ] + } + + + + + + + + +Butler, et al. Standards Track [Page 23] + +RFC 7946 GeoJSON August 2016 + + +A.4. MultiPoints + + Coordinates of a MultiPoint are an array of positions: + + { + "type": "MultiPoint", + "coordinates": [ + [100.0, 0.0], + [101.0, 1.0] + ] + } + +A.5. MultiLineStrings + + Coordinates of a MultiLineString are an array of LineString + coordinate arrays: + + { + "type": "MultiLineString", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 1.0] + ], + [ + [102.0, 2.0], + [103.0, 3.0] + ] + ] + } + + + + + + + + + + + + + + + + + + + + + +Butler, et al. Standards Track [Page 24] + +RFC 7946 GeoJSON August 2016 + + +A.6. MultiPolygons + + Coordinates of a MultiPolygon are an array of Polygon coordinate + arrays: + + { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [102.0, 2.0], + [103.0, 2.0], + [103.0, 3.0], + [102.0, 3.0], + [102.0, 2.0] + ] + ], + [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ], + [ + [100.2, 0.2], + [100.2, 0.8], + [100.8, 0.8], + [100.8, 0.2], + [100.2, 0.2] + ] + ] + ] + } + + + + + + + + + + + + + + + + +Butler, et al. Standards Track [Page 25] + +RFC 7946 GeoJSON August 2016 + + +A.7. GeometryCollections + + Each element in the "geometries" array of a GeometryCollection is one + of the Geometry objects described above: + + { + "type": "GeometryCollection", + "geometries": [{ + "type": "Point", + "coordinates": [100.0, 0.0] + }, { + "type": "LineString", + "coordinates": [ + [101.0, 0.0], + [102.0, 1.0] + ] + }] + } + +Appendix B. Changes from the Pre-IETF GeoJSON Format Specification + + This appendix briefly summarizes non-editorial changes from the 2008 + specification [GJ2008]. + +B.1. Normative Changes + + o Specification of coordinate reference systems has been removed, + i.e., the "crs" member of [GJ2008] is no longer used. + + o In the absence of elevation values, applications sensitive to + height or depth SHOULD interpret positions as being at local + ground or sea level (see Section 4). + + o Implementations SHOULD NOT extend position arrays beyond 3 + elements (see Section 3.1.1). + + o A line between two positions is a straight Cartesian line (see + Section 3.1.1). + + o Polygon rings MUST follow the right-hand rule for orientation + (counterclockwise external rings, clockwise internal rings). + + o The values of a "bbox" array are "[west, south, east, north]", not + "[minx, miny, maxx, maxy]" (see Section 5). + + o A Feature object's "id" member is a string or number (see + Section 3.2). + + + + +Butler, et al. Standards Track [Page 26] + +RFC 7946 GeoJSON August 2016 + + + o Extensions MAY be used, but MUST NOT change the semantics of + GeoJSON members and types (see Section 6). + + o GeoJSON objects MUST NOT contain the defining members of other + types (see Section 7.1). + + o The media type for GeoJSON is "application/geo+json". + +B.2. Informative Changes + + o The definition of a GeoJSON text has been added. + + o Rules for mapping 'geo' URIs have been added. + + o A recommendation of the I-JSON [RFC7493] constraints has been + added. + + o Implementers are cautioned about the effect of excessive + coordinate precision on interoperability. + + o Interoperability concerns of GeometryCollections are noted. These + objects should be used sparingly (see Section 3.1.8). + +Appendix C. GeoJSON Text Sequences + + All GeoJSON objects defined in this specification -- + FeatureCollection, Feature, and Geometry -- consist of exactly one + JSON object. However, there may be circumstances in which + applications need to represent sets or sequences of these objects + (over and above the grouping of Feature objects in a + FeatureCollection), e.g., in order to efficiently "stream" large + numbers of Feature objects. The definition of such sets or sequences + is outside the scope of this specification. + + If such a representation is needed, a new media type is required that + has the ability to represent these sets or sequences. When defining + such a media type, it may be useful to base it on "JavaScript Object + Notation (JSON) Text Sequences" [RFC7464], leaving the foundations of + how to represent multiple JSON objects to that specification, and + only defining how it applies to GeoJSON objects. + +Acknowledgements + + The GeoJSON format is the product of discussion on the GeoJSON + mailing list, , before October 2015 and in the IETF's GeoJSON + WG after October 2015. + + + + +Butler, et al. Standards Track [Page 27] + +RFC 7946 GeoJSON August 2016 + + + Material in this document was adapted with changes from + [GJ2008], which is licensed + under . + +Authors' Addresses + + Howard Butler + Hobu Inc. + + Email: howard@hobu.co + + + Martin Daly + Cadcorp + + Email: martin.daly@cadcorp.com + + + Allan Doyle + + Email: adoyle@intl-interfaces.com + + + Sean Gillies + Mapbox + + Email: sean.gillies@gmail.com + URI: http://sgillies.net + + + Stefan Hagen + Rheinaustr. 62 + Bonn 53225 + Germany + + Email: stefan@hagen.link + URI: http://stefan-hagen.website/ + + + Tim Schaub + Planet Labs + + Email: tim.schaub@gmail.com + + + + + + + + +Butler, et al. Standards Track [Page 28] + diff --git a/phpunit.xml b/phpunit.xml index 1ef0299..1e5d4dc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,14 +4,14 @@ bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache" executionOrder="depends,defects" - requireCoverageMetadata="true" - beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true" - failOnRisky="true" failOnWarning="true"> - - tests/phpGPX + + tests/Unit + + + tests/Integration @@ -19,8 +19,5 @@ src - - tests/phpGPX - diff --git a/src/phpGPX/GpxSerializable.php b/src/phpGPX/GpxSerializable.php index be07cc9..92f6f8c 100644 --- a/src/phpGPX/GpxSerializable.php +++ b/src/phpGPX/GpxSerializable.php @@ -4,7 +4,7 @@ interface GpxSerializable { - public static function gpxSerialize(\SimpleXMLElement $node); + public static function gpxSerialize(\SimpleXMLElement $node): void; - public function gpxDeserialize(\DOMDocument &$document); -} \ No newline at end of file + public function gpxDeserialize(\DOMDocument &$document): void; +} diff --git a/src/phpGPX/Helpers/DateTimeHelper.php b/src/phpGPX/Helpers/DateTimeHelper.php index c69ce57..03355fa 100644 --- a/src/phpGPX/Helpers/DateTimeHelper.php +++ b/src/phpGPX/Helpers/DateTimeHelper.php @@ -31,16 +31,16 @@ public static function comparePointsByTimestamp(Point $point1, Point $point2): b /** * @param $datetime * @param string $format - * @param string $timezone + * @param string|null $timezone * @return null|string * @throws \Exception */ - public static function formatDateTime($datetime, string $format = 'c', string $timezone = 'UTC'): ?string + public static function formatDateTime($datetime, string $format = 'c', ?string $timezone = 'UTC'): ?string { $formatted = null; if ($datetime instanceof \DateTime) { - $datetime->setTimezone(new \DateTimeZone($timezone)); + $datetime->setTimezone(new \DateTimeZone($timezone ?? 'UTC')); $formatted = $datetime->format($format); } diff --git a/src/phpGPX/Helpers/SerializationHelper.php b/src/phpGPX/Helpers/SerializationHelper.php index 30cbc69..21736c7 100644 --- a/src/phpGPX/Helpers/SerializationHelper.php +++ b/src/phpGPX/Helpers/SerializationHelper.php @@ -6,11 +6,9 @@ namespace phpGPX\Helpers; -use phpGPX\Models\Summarizable; - /** * Class SerializationHelper - * Contains basic serialization helpers used in summary() methods. + * Contains basic serialization helpers used in serialization methods. * @package phpGPX\Helpers */ abstract class SerializationHelper @@ -47,22 +45,30 @@ public static function stringOrNull($value): ?string } /** - * Recursively traverse Summarizable objects and returns their array representation according summary() method. - * @param Summarizable|Summarizable[] $object + * Recursively traverse objects and returns their array representation. + * If the object has a toArray method, it will be used, otherwise jsonSerialize will be used. + * @param \JsonSerializable|array|null $object * @return array|null */ - public static function serialize(Summarizable|array|null $object): ?array + public static function serialize(\JsonSerializable|array|null $object): ?array { if (is_array($object)) { $result = []; foreach ($object as $record) { - $result[] = $record->toArray(); + if (method_exists($record, 'toArray')) { + $result[] = $record->toArray(); + } else { + $result[] = $record->jsonSerialize(); + } $record = null; } $object = null; return $result; } else { - return $object?->toArray(); + if ($object !== null && method_exists($object, 'toArray')) { + return $object->toArray(); + } + return $object?->jsonSerialize(); } } @@ -72,7 +78,7 @@ public static function filterNotNull(array $array): array if (!is_array($item)) { continue; } - + $item = self::filterNotNull($item); } diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php index b697c71..6e69ab9 100644 --- a/src/phpGPX/Models/Bounds.php +++ b/src/phpGPX/Models/Bounds.php @@ -72,4 +72,16 @@ public static function parse(\SimpleXMLElement $node): ?Bounds (float) $node['maxlon'] ); } + + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // This method is required by the GpxSerializable interface + // but is not used in this class + } + + public function gpxDeserialize(\DOMDocument &$document): void + { + // This method is required by the GpxSerializable interface + // but is not used in this class + } } diff --git a/src/phpGPX/Models/Collection.php b/src/phpGPX/Models/Collection.php index 66b667b..9d2accf 100644 --- a/src/phpGPX/Models/Collection.php +++ b/src/phpGPX/Models/Collection.php @@ -10,7 +10,7 @@ * Class Collection * @package phpGPX\Models */ -abstract class Collection implements Summarizable, StatsCalculator +abstract class Collection implements \JsonSerializable, StatsCalculator { /** diff --git a/src/phpGPX/Models/Copyright.php b/src/phpGPX/Models/Copyright.php index 78d1836..fff25fb 100644 --- a/src/phpGPX/Models/Copyright.php +++ b/src/phpGPX/Models/Copyright.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; use phpGPX\Helpers\SerializationHelper; /** @@ -14,26 +15,26 @@ * By linking to an appropriate license, you may place your data into the public domain or grant additional usage rights. * @package phpGPX\Models */ -class Copyright implements Summarizable +class Copyright implements \JsonSerializable, GpxSerializable { /** * Copyright holder (TopoSoft, Inc.) - * @var string + * @var string|null */ - public $author; + public ?string $author; /** * Year of copyright. - * @var string + * @var string|null */ - public $year; + public ?string $year; /** * Link to external file containing license text. - * @var string + * @var string|null */ - public $license; + public ?string $license; /** * Copyright constructor. @@ -50,7 +51,7 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'author' => $this->author, @@ -58,4 +59,33 @@ public function toArray() 'license' => SerializationHelper::stringOrNull($this->license) ]; } + + /** + * Implements JsonSerializable interface + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Email.php b/src/phpGPX/Models/Email.php index fbb0155..c529dff 100644 --- a/src/phpGPX/Models/Email.php +++ b/src/phpGPX/Models/Email.php @@ -6,24 +6,26 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; + /** * Class Email * An email address. Broken into two parts (id and domain) to help prevent email harvesting. * @package phpGPX\Models */ -class Email implements Summarizable +class Email implements \JsonSerializable, GpxSerializable { /** * Id half of email address (jakub.dubec) - * @var string + * @var string|null */ - public $id; + public ?string $id = null; /** Domain half of email address (gmail.com) - * @var string + * @var string|null */ - public $domain; + public ?string $domain = null; /** * Email constructor. @@ -39,11 +41,40 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ - 'id' => (string) $this->id, - 'domain' => (string) $this->domain + 'id' => $this->id !== null ? (string) $this->id : null, + 'domain' => $this->domain !== null ? (string) $this->domain : null ]; } + + /** + * Serialize object to array for JSON encoding + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Extensions.php b/src/phpGPX/Models/Extensions.php index 5eb37fa..11cca7c 100644 --- a/src/phpGPX/Models/Extensions.php +++ b/src/phpGPX/Models/Extensions.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; use phpGPX\Helpers\SerializationHelper; use phpGPX\Models\Extensions\TrackPointExtension; @@ -14,29 +15,66 @@ * TODO: http://www.garmin.com/xmlschemas/GpxExtensions/v3 * @package phpGPX\Models */ -class Extensions implements Summarizable +class Extensions implements \JsonSerializable, GpxSerializable { /** * GPX Garmin TrackPointExtension v1 * @see 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1' - * @var TrackPointExtension + * @var TrackPointExtension|null */ - public $trackPointExtension; + public ?TrackPointExtension $trackPointExtension; /** - * @var [] + * @var array */ - public $unsupported = []; + public array $unsupported = []; + + /** + * Extensions constructor. + */ + public function __construct() + { + $this->trackPointExtension = null; + } /** * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'trackpoint' => SerializationHelper::serialize($this->trackPointExtension), 'unsupported' => $this->unsupported, ]; } + + /** + * Implements JsonSerializable interface + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Extensions/AbstractExtension.php b/src/phpGPX/Models/Extensions/AbstractExtension.php index 06ce317..4ca8138 100644 --- a/src/phpGPX/Models/Extensions/AbstractExtension.php +++ b/src/phpGPX/Models/Extensions/AbstractExtension.php @@ -6,9 +6,9 @@ namespace phpGPX\Models\Extensions; -use phpGPX\Models\Summarizable; +use phpGPX\GpxSerializable; -abstract class AbstractExtension implements Summarizable +abstract class AbstractExtension implements \JsonSerializable, GpxSerializable { /** @@ -33,4 +33,24 @@ public function __construct(string $namespace, string $extensionName) $this->namespace = $namespace; $this->extensionName = $extensionName; } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php index 89b2046..09f3424 100644 --- a/src/phpGPX/Models/Extensions/TrackPointExtension.php +++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php @@ -84,7 +84,7 @@ public function __construct() * @return array */ public function toArray(): array - { + { return [ 'aTemp' => $this->aTemp ?? null, 'wTemp' => $this->wTemp ?? null, @@ -96,4 +96,13 @@ public function toArray(): array 'bearing' => $this->bearing ?? null ]; } + + /** + * Serialize object to array for JSON encoding + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } } diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php index abc3a6e..c4211f6 100644 --- a/src/phpGPX/Models/GpxFile.php +++ b/src/phpGPX/Models/GpxFile.php @@ -19,43 +19,43 @@ * Representation of GPX file. * @package phpGPX\Models */ -class GpxFile implements Summarizable +class GpxFile implements \JsonSerializable, \phpGPX\GpxSerializable { /** * A list of waypoints. * @var Point[] */ - public $waypoints; + public array $waypoints; /** * A list of routes. * @var Route[] */ - public $routes; + public array $routes; /** * A list of tracks. * @var Track[] */ - public $tracks; + public array $tracks; /** * Metadata about the file. * The original GPX 1.1 attribute. * @var Metadata|null */ - public $metadata; + public ?Metadata $metadata; /** * @var Extensions|null */ - public $extensions; + public ?Extensions $extensions; /** * Creator of GPX file. * @var string|null */ - public $creator; + public ?string $creator; /** * GpxFile constructor. @@ -75,7 +75,7 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return SerializationHelper::filterNotNull([ 'creator' => SerializationHelper::stringOrNull($this->creator), @@ -87,20 +87,84 @@ public function toArray() ]); } + /** + * Serialize object to array for JSON encoding + * Always returns GeoJSON format + * @return array + */ + public function jsonSerialize(): array + { + // GeoJSON FeatureCollection format + $features = []; + + // Add waypoints as Point features - each waypoint handles its own serialization + foreach ($this->waypoints as $waypoint) { + $features[] = $waypoint->jsonSerialize(); + } + + // Add routes as LineString features - each route handles its own serialization + foreach ($this->routes as $route) { + $features[] = $route->jsonSerialize(); + } + + // Add tracks as MultiLineString features - each track handles its own serialization + foreach ($this->tracks as $track) { + $features[] = $track->jsonSerialize(); + } + + return [ + 'type' => 'FeatureCollection', + 'features' => $features, + 'metadata' => SerializationHelper::serialize($this->metadata) + ]; + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation of GpxSerializable interface + // This method would be called to serialize a GpxFile to GPX XML + // Since the toXML method already handles this, this method can be empty + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation of GpxSerializable interface + // This method would be called to deserialize GPX XML to a GpxFile + // Since the parse method in phpGPX class already handles this, this method can be empty + } + + /** * Return JSON representation of GPX file with statistics. + * @param bool $geojson Whether to return GeoJSON format (true) or GPX format (false) * @return string */ - public function toJSON() + public function toJSON(bool $geojson = true): string { - return json_encode($this->toArray(), phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : null); + if ($geojson) { + // GeoJSON format (using jsonSerialize) + return json_encode($this->jsonSerialize(), phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : null); + } else { + // GPX format (using toArray) + return json_encode($this->toArray(), phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : null); + } } /** * Create XML representation of GPX file. * @return \DOMDocument */ - public function toXML() + public function toXML(): \DOMDocument { $document = new \DOMDocument("1.0", 'UTF-8'); @@ -167,7 +231,7 @@ public function toXML() * @param string $path * @param string $format */ - public function save($path, $format) + public function save(string $path, string $format): void { switch ($format) { case phpGPX::XML_FORMAT: @@ -175,10 +239,15 @@ public function save($path, $format) $document->save($path); break; case phpGPX::JSON_FORMAT: - file_put_contents($path, $this->toJSON()); + // Use GPX format for JSON + file_put_contents($path, $this->toJSON(false)); + break; + case phpGPX::GEOJSON_FORMAT: + // Use GeoJSON format + file_put_contents($path, $this->toJSON(true)); break; default: throw new \RuntimeException("Unsupported file format!"); - }; + } } } diff --git a/src/phpGPX/Models/Link.php b/src/phpGPX/Models/Link.php index bccdd3a..df09259 100644 --- a/src/phpGPX/Models/Link.php +++ b/src/phpGPX/Models/Link.php @@ -6,32 +6,34 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; + /** * Class Link according to GPX 1.1 specification. * A link to an external resource (Web page, digital photo, video clip, etc) with additional information. * @see http://www.topografix.com/GPX/1/1/#type_linkType * @package phpGPX\Models */ -class Link implements Summarizable +class Link implements \JsonSerializable, GpxSerializable { /** * URL of hyperlink. - * @var string + * @var string|null */ - public $href; + public ?string $href = null; /** * Text of hyperlink. * @var string|null */ - public $text; + public ?string $text; /** * Mime type of content (image/jpeg) * @var string|null */ - public $type; + public ?string $type; /** * Link constructor. @@ -48,12 +50,41 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ - 'href' => (string) $this->href, + 'href' => $this->href !== null ? (string) $this->href : null, 'text' => $this->text, 'type' => $this->type ]; } + + /** + * Implements JsonSerializable interface + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Metadata.php b/src/phpGPX/Models/Metadata.php index 582b62a..05ac2d7 100644 --- a/src/phpGPX/Models/Metadata.php +++ b/src/phpGPX/Models/Metadata.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; use phpGPX\Helpers\DateTimeHelper; use phpGPX\Helpers\SerializationHelper; @@ -15,7 +16,7 @@ * Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. * @package phpGPX\Models */ -class Metadata implements Summarizable +class Metadata implements \JsonSerializable, GpxSerializable { /** @@ -23,59 +24,59 @@ class Metadata implements Summarizable * Original GPX 1.1 attribute. * @var string|null */ - public $name; + public ?string $name; /** * A description of the contents of the GPX file. * Original GPX 1.1 attribute. * @var string|null */ - public $description; + public ?string $description; /** * The person or organization who created the GPX file. * An original GPX 1.1 attribute. * @var Person|null */ - public $author; + public ?Person $author; /** * Copyright and license information governing use of the file. * Original GPX 1.1 attribute. * @var Copyright|null */ - public $copyright; + public ?Copyright $copyright; /** * Original GPX 1.1 attribute. * @var Link[]|null */ - public $links; + public ?array $links; /** * Date of GPX creation - * @var \DateTime + * @var \DateTime|null */ - public $time; + public ?\DateTime $time; /** * Keywords associated with the file. Search engines or databases can use this information to classify the data. * @var string|null */ - public $keywords; + public ?string $keywords; /** * Minimum and maximum coordinates which describe the extent of the coordinates in the file. * Original GPX 1.1 attribute. * @var Bounds|null */ - public $bounds; + public ?Bounds $bounds; /** * Extensions. * @var Extensions|null */ - public $extensions; + public ?Extensions $extensions; /** * Metadata constructor. @@ -98,7 +99,7 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'name' => SerializationHelper::stringOrNull($this->name), @@ -112,4 +113,33 @@ public function toArray() 'extensions' => SerializationHelper::serialize($this->extensions) ]; } + + /** + * Implements JsonSerializable interface + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Person.php b/src/phpGPX/Models/Person.php index 29e6675..2709121 100644 --- a/src/phpGPX/Models/Person.php +++ b/src/phpGPX/Models/Person.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; use phpGPX\Helpers\SerializationHelper; /** @@ -13,29 +14,29 @@ * A person or organisation * @package phpGPX\Models */ -class Person implements Summarizable +class Person implements \JsonSerializable, GpxSerializable { /** * Name of person or organization. * An original GPX 1.1 attribute. - * @var string + * @var string|null */ - public $name; + public ?string $name; /** * E-mail address. * An original GPX 1.1 attribute. * @var Email|null */ - public $email; + public ?Email $email; /** * Link to Web site or other external information about person. * An original GPX 1.1 attribute. - * @var Link[] + * @var Link[]|null */ - public $links; + public ?array $links; /** * Person constructor. @@ -52,7 +53,7 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'name' => (string) $this->name, @@ -60,4 +61,33 @@ public function toArray() 'links' => SerializationHelper::serialize($this->links) ]; } + + /** + * Implements JsonSerializable interface + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index d18c984..0909a7b 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -23,7 +23,7 @@ enum PointType: string * @see http://www.topografix.com/GPX/1/1/#type_wptType * @package phpGPX\Models */ -class Point implements Summarizable +class Point implements \JsonSerializable, \phpGPX\GpxSerializable { const WAYPOINT = 'waypoint'; const TRACKPOINT = 'track'; @@ -32,44 +32,44 @@ class Point implements Summarizable /** * The latitude of the point. Decimal degrees, WGS84 datum. * Original GPX 1.1 attribute. - * @var float + * @var float|null */ - public $latitude; + public ?float $latitude; /** * The longitude of the point. Decimal degrees, WGS84 datum. * Original GPX 1.1 attribute. - * @var float + * @var float|null */ - public $longitude; + public ?float $longitude; /** * Elevation (in meters) of the point. * Original GPX 1.1 attribute. * @var float|null */ - public $elevation; + public ?float $elevation; /** * Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! * Fractional seconds are allowed for millisecond timing in tracklogs. * @var \DateTime|null */ - public $time; + public ?\DateTime $time; /** * Magnetic variation (in degrees) at the point * Original GPX 1.1 attribute. * @var float|null */ - public $magVar; + public ?float $magVar; /** * Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. * Original GPX 1.1 attribute. * @var float|null */ - public $geoidHeight; + public ?float $geoidHeight; /** * The GPS name of the waypoint. This field will be transferred to and from the GPS. @@ -78,35 +78,35 @@ class Point implements Summarizable * Original GPX 1.1 attribute. * @var string|null */ - public $name; + public ?string $name; /** * GPS waypoint comment. Sent to GPS as comment. * Original GPX 1.1 attribute. * @var string|null */ - public $comment; + public ?string $comment; /** * A text description of the element. Holds additional information about the element intended for the user, not the GPS. * Original GPX 1.1 attribute. * @var string|null */ - public $description; + public ?string $description; /** * Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. * Original GPX 1.1 attribute. * @var string|null */ - public $source; + public ?string $source; /** * Link to additional information about the waypoint. * Original GPX 1.1 attribute. * @var Link[] */ - public $links; + public array $links; /** * Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. @@ -114,98 +114,98 @@ class Point implements Summarizable * Original GPX 1.1 attribute. * @var string|null */ - public $symbol; + public ?string $symbol; /** * Type (classification) of the waypoint. * Original GPX 1.1 attribute. * @var string|null */ - public $type; + public ?string $type; /** * Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used * Possible values: {'none'|'2d'|'3d'|'dgps'|'pps'} * Original GPX 1.1 attribute. * @see http://www.topografix.com/GPX/1/1/#type_fixType - * @var string + * @var string|null */ - public $fix; + public ?string $fix; /** * Number of satellites used to calculate the GPX fix. Always positive value. * Original GPX 1.1 attribute. - * @var integer + * @var integer|null */ - public $satellitesNumber; + public ?int $satellitesNumber; /** * Horizontal dilution of precision. * Original GPX 1.1 attribute. - * @var float + * @var float|null */ - public $hdop; + public ?float $hdop; /** * Vertical dilution of precision. * Original GPX 1.1 attribute. - * @var float + * @var float|null */ - public $vdop; + public ?float $vdop; /** * Position dilution of precision. * Original GPX 1.1 attribute - * @var float + * @var float|null */ - public $pdop; + public ?float $pdop; /** * Number of seconds since last DGPS update. * Original GPX 1.1 attribute. - * @var integer + * @var integer|null */ - public $ageOfGpsData; + public ?int $ageOfGpsData; /** * ID of DGPS station used in differential correction. * Original GPX 1.1 attribute. * @see http://www.topografix.com/GPX/1/1/#type_dgpsStationType - * @var integer + * @var integer|null */ - public $dgpsid; + public ?int $dgpsid; /** * Difference in in distance (in meters) between last point. * Value is created by phpGPX library. - * @var float + * @var float|null */ - public $difference; + public ?float $difference; /** * Distance from collection start in meters. * Value is created by phpGPX library. - * @var float + * @var float|null */ - public $distance; + public ?float $distance; /** * Objects stores GPX extensions from another namespaces. - * @var Extensions + * @var Extensions|null */ - public $extensions; + public ?Extensions $extensions; /** * Type of the point (parent collation type (ROUTE|WAYPOINT|TRACK)) * @var string */ - private $pointType; + private string $pointType; /** * Point constructor. * @param string $pointType */ - public function __construct($pointType) + public function __construct(string $pointType) { $this->latitude = null; $this->longitude = null; @@ -237,16 +237,91 @@ public function __construct($pointType) * Return point type (ROUTE|TRACK|WAYPOINT) * @return string */ - public function getPointType() + public function getPointType(): string { return $this->pointType; } + /** + * Serialize object to array for JSON encoding + * Always returns GeoJSON format + * @return array + */ + public function jsonSerialize(): array + { + // GeoJSON Point format + $properties = [ + 'ele' => SerializationHelper::floatOrNull($this->elevation), + 'time' => DateTimeHelper::formatDateTime($this->time, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), + 'magvar' => SerializationHelper::floatOrNull($this->magVar), + 'geoidheight' => SerializationHelper::floatOrNull($this->geoidHeight), + 'name' => SerializationHelper::stringOrNull($this->name), + 'cmt' => SerializationHelper::stringOrNull($this->comment), + 'desc' => SerializationHelper::stringOrNull($this->description), + 'src' => SerializationHelper::stringOrNull($this->source), + 'link' => SerializationHelper::serialize($this->links), + 'sym' => SerializationHelper::stringOrNull($this->symbol), + 'type' => SerializationHelper::stringOrNull($this->type), + 'fix' => SerializationHelper::stringOrNull($this->fix), + 'sat' => SerializationHelper::integerOrNull($this->satellitesNumber), + 'hdop' => SerializationHelper::floatOrNull($this->hdop), + 'vdop' => SerializationHelper::floatOrNull($this->vdop), + 'pdop' => SerializationHelper::floatOrNull($this->pdop), + 'ageofdgpsdata' => SerializationHelper::floatOrNull($this->ageOfGpsData), + 'dgpsid' => SerializationHelper::integerOrNull($this->dgpsid), + 'difference' => SerializationHelper::floatOrNull($this->difference), + 'distance' => SerializationHelper::floatOrNull($this->distance), + 'extensions' => SerializationHelper::serialize($this->extensions) + ]; + + // Filter out null values + $properties = array_filter($properties, function ($value) { + return $value !== null; + }); + + return [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'Point', + 'coordinates' => [ + (float) $this->longitude, + (float) $this->latitude, + SerializationHelper::floatOrNull($this->elevation) + ] + ], + 'properties' => $properties + ]; + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation of GpxSerializable interface + // This method would be called to serialize a Point to GPX XML + // Since PointParser already handles this, this method can be empty + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation of GpxSerializable interface + // This method would be called to deserialize GPX XML to a Point + // Since PointParser already handles this, this method can be empty + } + /** * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'lat' => (float) $this->latitude, diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index d0ecf2e..8a86ad5 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -16,7 +16,7 @@ * Class Route * @package phpGPX\Models */ -class Route extends Collection +class Route extends Collection implements \phpGPX\GpxSerializable { /** @@ -24,7 +24,7 @@ class Route extends Collection * An original GPX 1.1 attribute. * @var Point[] */ - public $points; + public array $points; /** * Route constructor. @@ -54,11 +54,84 @@ public function getPoints(): array return $points; } + /** + * Serialize object to array for JSON encoding + * Always returns GeoJSON format + * @return array + */ + public function jsonSerialize(): array + { + // GeoJSON LineString feature + $coordinates = []; + $properties = [ + 'name' => SerializationHelper::stringOrNull($this->name), + 'cmt' => SerializationHelper::stringOrNull($this->comment), + 'desc' => SerializationHelper::stringOrNull($this->description), + 'src' => SerializationHelper::stringOrNull($this->source), + 'link' => SerializationHelper::serialize($this->links), + 'number' => SerializationHelper::integerOrNull($this->number), + 'type' => SerializationHelper::stringOrNull($this->type), + 'extensions' => SerializationHelper::serialize($this->extensions) + ]; + + // Filter out null values + $properties = array_filter($properties, function ($value) { + return $value !== null; + }); + + // Add stats if available + if ($this->stats) { + $properties['stats'] = $this->stats->jsonSerialize(); + } + + // Collect coordinates from route points + foreach ($this->points as $point) { + $coordinates[] = [ + (float) $point->longitude, + (float) $point->latitude, + SerializationHelper::floatOrNull($point->elevation) + ]; + } + + return [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'LineString', + 'coordinates' => $coordinates + ], + 'properties' => $properties + ]; + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + // This method would be called to serialize a Route to GPX XML + // Since RouteParser already handles this, this method can be empty + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + // This method would be called to deserialize GPX XML to a Route + // Since RouteParser already handles this, this method can be empty + } + /** * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'name' => SerializationHelper::stringOrNull($this->name), @@ -78,7 +151,7 @@ public function toArray() * Recalculate stats objects. * @return void */ - public function recalculateStats() + public function recalculateStats(): void { if (empty($this->stats)) { $this->stats = new Stats(); diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index 0e34faf..b46b156 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Helpers\GeoHelper; @@ -19,24 +20,24 @@ * start a new Track Segment for each continuous span of track data. * @package phpGPX\Models */ -class Segment implements Summarizable, StatsCalculator +class Segment implements \JsonSerializable, GpxSerializable, StatsCalculator { /** * Array of segment points * @var Point[] */ - public $points; + public array $points; /** * You can add extend GPX by adding your own elements from another schema here. * @var Extensions|null */ - public $extensions; + public ?Extensions $extensions; /** * @var Stats|null */ - public $stats; + public ?Stats $stats; /** * Segment constructor. @@ -53,7 +54,7 @@ public function __construct() * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'points' => SerializationHelper::serialize($this->points), @@ -62,10 +63,73 @@ public function toArray() ]; } + /** + * Implements JsonSerializable interface + * Always returns GeoJSON format + * @return array + */ + public function jsonSerialize(): array + { + // GeoJSON LineString feature + $coordinates = []; + $properties = [ + 'extensions' => SerializationHelper::serialize($this->extensions) + ]; + + // Filter out null values + $properties = array_filter($properties, function ($value) { + return $value !== null; + }); + + // Add stats if available + if ($this->stats) { + $properties['stats'] = $this->stats->jsonSerialize(); + } + + // Collect coordinates from segment points + foreach ($this->points as $point) { + $coordinates[] = [ + (float) $point->longitude, + (float) $point->latitude, + SerializationHelper::floatOrNull($point->elevation) + ]; + } + + return [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'LineString', + 'coordinates' => $coordinates + ], + 'properties' => $properties + ]; + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } + + /** * @return array|Point[] */ - public function getPoints() + public function getPoints(): array { return $this->points; } @@ -74,7 +138,7 @@ public function getPoints() * Recalculate stats objects. * @return void */ - public function recalculateStats() + public function recalculateStats(): void { if (empty($this->stats)) { $this->stats = new Stats(); diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index 89ae6ac..6a59e84 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\GpxSerializable; use phpGPX\Helpers\DateTimeHelper; use phpGPX\phpGPX; @@ -13,103 +14,104 @@ * Class Stats * @package phpGPX\Models */ -class Stats implements Summarizable +class Stats implements \JsonSerializable, GpxSerializable { /** * Distance in meters (m) - * @var float + * @var float|null */ - public $distance = 0; + public ?float $distance = null; /** * Distance in meters (m) including elevation loss/gain - * @var float + * @var float|null */ - public $realDistance = 0; + public ?float $realDistance = null; /** * Average speed in meters per second (m/s) - * @var float + * @var float|null */ - public $averageSpeed = null; + public ?float $averageSpeed = null; /** * Average pace in seconds per kilometer (s/km) - * @var float + * @var float|null */ - public $averagePace = null; + public ?float $averagePace = null; /** * Minimal altitude in meters (m) - * @var int + * @var float|null */ - public $minAltitude = null; + public ?float $minAltitude = null; /** * Minimal altitude coordinate - * @var [float,float] + * @var array|null */ - public $minAltitudeCoords = null; + public ?array $minAltitudeCoords = null; /** * Maximal altitude in meters (m) - * @var int + * @var float|null */ - public $maxAltitude = null; + public ?float $maxAltitude = null; /** * Maximal altitude coordinate - * @var [float,float] + * @var array|null */ - public $maxAltitudeCoords = null; + public ?array $maxAltitudeCoords = null; /** * Cumulative elevation gain in meters (m) - * @var int + * @var float|null */ - public $cumulativeElevationGain = null; + public ?float $cumulativeElevationGain = null; /** * Cumulative elevation loss in meters (m) - * @var int + * @var float|null */ - public $cumulativeElevationLoss = null; + public ?float $cumulativeElevationLoss = null; /** * Started time - * @var \DateTime + * @var \DateTime|null */ - public $startedAt = null; + public ?\DateTime $startedAt = null; /** * startedAt coordinate - * @var [float,float] + * @var array|null */ - public $startedAtCoords = null; + public ?array $startedAtCoords = null; /** * Ending time - * @var \DateTime + * @var \DateTime|null */ - public $finishedAt = null; + public ?\DateTime $finishedAt = null; /** * finishedAt coordinate - * @var [float,float] + * @var array|null */ - public $finishedAtCoords = null; + public ?array $finishedAtCoords = null; /** * Duration is seconds - * @var int + * @var float|null */ - public $duration = null; + public ?float $duration = null; /** * Reset all stats + * @return void */ - public function reset() + public function reset(): void { $this->distance = null; $this->realDistance = null; @@ -125,30 +127,60 @@ public function reset() $this->startedAtCoords = null; $this->finishedAt = null; $this->finishedAtCoords = null; + $this->duration = null; } /** * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ - 'distance' => (float)$this->distance, - 'realDistance' => (float)$this->realDistance, - 'avgSpeed' => (float)$this->averageSpeed, - 'avgPace' => (float)$this->averagePace, - 'minAltitude' => (float)$this->minAltitude, + 'distance' => $this->distance !== null ? (float)$this->distance : null, + 'realDistance' => $this->realDistance !== null ? (float)$this->realDistance : null, + 'avgSpeed' => $this->averageSpeed !== null ? (float)$this->averageSpeed : null, + 'avgPace' => $this->averagePace !== null ? (float)$this->averagePace : null, + 'minAltitude' => $this->minAltitude !== null ? (float)$this->minAltitude : null, 'minAltitudeCoords' => $this->minAltitudeCoords, - 'maxAltitude' => (float)$this->maxAltitude, + 'maxAltitude' => $this->maxAltitude !== null ? (float)$this->maxAltitude : null, 'maxAltitudeCoords' => $this->maxAltitudeCoords, - 'cumulativeElevationGain' => (float)$this->cumulativeElevationGain, - 'cumulativeElevationLoss' => (float)$this->cumulativeElevationLoss, - 'startedAt' => DateTimeHelper::formatDateTime($this->startedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), + 'cumulativeElevationGain' => $this->cumulativeElevationGain !== null ? (float)$this->cumulativeElevationGain : null, + 'cumulativeElevationLoss' => $this->cumulativeElevationLoss !== null ? (float)$this->cumulativeElevationLoss : null, + 'startedAt' => $this->startedAt !== null ? DateTimeHelper::formatDateTime($this->startedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT) : null, 'startedAtCoords' => $this->startedAtCoords, - 'finishedAt' => DateTimeHelper::formatDateTime($this->finishedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), + 'finishedAt' => $this->finishedAt !== null ? DateTimeHelper::formatDateTime($this->finishedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT) : null, 'finishedAtCoords' => $this->finishedAtCoords, - 'duration' => (float)$this->duration + 'duration' => $this->duration !== null ? (float)$this->duration : null ]; } + + /** + * Implements JsonSerializable interface + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + } } diff --git a/src/phpGPX/Models/StatsCalculator.php b/src/phpGPX/Models/StatsCalculator.php index f5cab09..e30e5d1 100644 --- a/src/phpGPX/Models/StatsCalculator.php +++ b/src/phpGPX/Models/StatsCalculator.php @@ -13,11 +13,11 @@ interface StatsCalculator * Recalculate stats objects. * @return void */ - public function recalculateStats(); + public function recalculateStats(): void; /** * Return all points in collection. * @return Point[] */ - public function getPoints(); + public function getPoints(): array; } diff --git a/src/phpGPX/Models/Summarizable.php b/src/phpGPX/Models/Summarizable.php index 1e4433b..e69de29 100644 --- a/src/phpGPX/Models/Summarizable.php +++ b/src/phpGPX/Models/Summarizable.php @@ -1,21 +0,0 @@ - - */ - -namespace phpGPX\Models; - -/** - * Interface Summarizable - * @package phpGPX\Models - */ -interface Summarizable -{ - - /** - * Serialize object to array - * @return array - */ - public function toArray(); -} diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index 7452cb1..a7d8b5d 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -14,14 +14,14 @@ * Class Track * @package phpGPX\Models */ -class Track extends Collection +class Track extends Collection implements \phpGPX\GpxSerializable { /** * Array of Track segments * @var Segment[] */ - public $segments; + public array $segments; /** * Track constructor. @@ -53,11 +53,90 @@ public function getPoints(): array return $points; } + /** + * Serialize object to array for JSON encoding + * Always returns GeoJSON format + * @return array + */ + public function jsonSerialize(): array + { + // GeoJSON MultiLineString feature + $segmentCoordinates = []; + $properties = [ + 'name' => SerializationHelper::stringOrNull($this->name), + 'cmt' => SerializationHelper::stringOrNull($this->comment), + 'desc' => SerializationHelper::stringOrNull($this->description), + 'src' => SerializationHelper::stringOrNull($this->source), + 'link' => SerializationHelper::serialize($this->links), + 'number' => SerializationHelper::integerOrNull($this->number), + 'type' => SerializationHelper::stringOrNull($this->type), + 'extensions' => SerializationHelper::serialize($this->extensions) + ]; + + // Filter out null values + $properties = array_filter($properties, function ($value) { + return $value !== null; + }); + + // Add stats if available + if ($this->stats) { + $properties['stats'] = $this->stats->jsonSerialize(); + } + + // Collect coordinates from track segments + foreach ($this->segments as $segment) { + $coordinates = []; + + foreach ($segment->points as $point) { + $coordinates[] = [ + (float) $point->longitude, + (float) $point->latitude, + SerializationHelper::floatOrNull($point->elevation) + ]; + } + + $segmentCoordinates[] = $coordinates; + } + + return [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'MultiLineString', + 'coordinates' => $segmentCoordinates + ], + 'properties' => $properties + ]; + } + + /** + * GPX serializer + * @param \SimpleXMLElement $node + * @return void + */ + public static function gpxSerialize(\SimpleXMLElement $node): void + { + // Implementation required by GpxSerializable interface + // This method would be called to serialize a Track to GPX XML + // Since TrackParser already handles this, this method can be empty + } + + /** + * GPX deserializer + * @param \DOMDocument $document + * @return void + */ + public function gpxDeserialize(\DOMDocument &$document): void + { + // Implementation required by GpxSerializable interface + // This method would be called to deserialize GPX XML to a Track + // Since TrackParser already handles this, this method can be empty + } + /** * Serialize object to array * @return array */ - public function toArray() + public function toArray(): array { return [ 'name' => SerializationHelper::stringOrNull($this->name), @@ -77,7 +156,7 @@ public function toArray() * Recalculate stats objects. * @return void */ - public function recalculateStats() + public function recalculateStats(): void { if (empty($this->stats)) { $this->stats = new Stats(); diff --git a/src/phpGPX/Parsers/BoundsParser.php b/src/phpGPX/Parsers/BoundsParser.php index d3657fb..21c7f2e 100644 --- a/src/phpGPX/Parsers/BoundsParser.php +++ b/src/phpGPX/Parsers/BoundsParser.php @@ -21,7 +21,7 @@ abstract class BoundsParser * @param \SimpleXMLElement $node * @return Bounds|null */ - public static function parse(\SimpleXMLElement $node) + public static function parse(\SimpleXMLElement $node): ?Bounds { if ($node->getName() != self::$tagName) { return null; @@ -41,23 +41,23 @@ public static function parse(\SimpleXMLElement $node) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Bounds $bounds, \DOMDocument &$document) + public static function toXML(Bounds $bounds, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); - if (!is_null($bounds->minLatitude)) { + if ($bounds->minLatitude !== null) { $node->setAttribute('minlat', $bounds->minLatitude); } - if (!is_null($bounds->minLongitude)) { + if ($bounds->minLongitude !== null) { $node->setAttribute('minlon', $bounds->minLongitude); } - if (!is_null($bounds->maxLatitude)) { + if ($bounds->maxLatitude !== null) { $node->setAttribute('maxlat', $bounds->maxLatitude); } - if (!is_null($bounds->maxLongitude)) { + if ($bounds->maxLongitude !== null) { $node->setAttribute('maxlon', $bounds->maxLongitude); } diff --git a/src/phpGPX/Parsers/EmailParser.php b/src/phpGPX/Parsers/EmailParser.php index 371b86a..ab0812f 100644 --- a/src/phpGPX/Parsers/EmailParser.php +++ b/src/phpGPX/Parsers/EmailParser.php @@ -20,7 +20,7 @@ abstract class EmailParser * @param \SimpleXMLElement $node * @return Email */ - public static function parse(\SimpleXMLElement $node) + public static function parse(\SimpleXMLElement $node): Email { $email = new Email(); @@ -36,15 +36,15 @@ public static function parse(\SimpleXMLElement $node) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Email $email, \DOMDocument &$document) + public static function toXML(Email $email, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); - if (!empty($email->id)) { + if ($email->id !== null && $email->id !== '') { $node->setAttribute('id', $email->id); } - if (!empty($email->domain)) { + if ($email->domain !== null && $email->domain !== '') { $node->setAttribute('domain', $email->domain); } diff --git a/src/phpGPX/Parsers/LinkParser.php b/src/phpGPX/Parsers/LinkParser.php index 66906b5..30042fc 100644 --- a/src/phpGPX/Parsers/LinkParser.php +++ b/src/phpGPX/Parsers/LinkParser.php @@ -13,12 +13,18 @@ abstract class LinkParser private static $tagName = 'link'; /** - * @param \SimpleXMLElement[] $nodes + * @param \SimpleXMLElement|\SimpleXMLElement[] $nodes * @return Link[] */ - public static function parse($nodes = []) + public static function parse($nodes): array { $links = []; + + // Handle both a single SimpleXMLElement and an array of SimpleXMLElements + if (!is_array($nodes)) { + $nodes = [$nodes]; + } + foreach ($nodes as $node) { $link = new Link(); $link->href = isset($node['href']) ? (string) $node['href'] : null; @@ -35,7 +41,7 @@ public static function parse($nodes = []) * @param \DOMDocument $document * @return \DOMElement[] */ - public static function toXMLArray(array $links, \DOMDocument &$document) + public static function toXMLArray(array $links, \DOMDocument &$document): array { $result = []; @@ -51,18 +57,20 @@ public static function toXMLArray(array $links, \DOMDocument &$document) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Link $link, \DOMDocument &$document) + public static function toXML(Link $link, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); - $node->setAttribute('href', $link->href); + if ($link->href !== null && $link->href !== '') { + $node->setAttribute('href', $link->href); + } - if (!empty($link->text)) { + if ($link->text !== null && $link->text !== '') { $child = $document->createElement('text', $link->text); $node->appendChild($child); } - if (!empty($link->type)) { + if ($link->type !== null && $link->type !== '') { $child = $document->createElement('type', $link->type); $node->appendChild($child); } diff --git a/src/phpGPX/Parsers/MetadataParser.php b/src/phpGPX/Parsers/MetadataParser.php index 58c7a95..885a245 100644 --- a/src/phpGPX/Parsers/MetadataParser.php +++ b/src/phpGPX/Parsers/MetadataParser.php @@ -86,9 +86,10 @@ public static function parse(\SimpleXMLElement $node) break; default: if (!in_array($attribute['type'], ['object', 'array'])) { - $metadata->{$attribute['name']} = isset($node->$key) ? $node->$key : null; - if (!is_null($metadata->{$attribute['name']})) { - settype($metadata->{$attribute['name']}, $attribute['type']); + if (isset($node->$key)) { + $value = (string) $node->$key; + settype($value, $attribute['type']); + $metadata->{$attribute['name']} = $value; } } break; diff --git a/src/phpGPX/Parsers/PointParser.php b/src/phpGPX/Parsers/PointParser.php index 731a48d..c81dede 100644 --- a/src/phpGPX/Parsers/PointParser.php +++ b/src/phpGPX/Parsers/PointParser.php @@ -96,7 +96,7 @@ abstract class PointParser 'rtept' => Point::ROUTEPOINT ]; - public static function parse(\SimpleXMLElement $node) + public static function parse(\SimpleXMLElement $node): ?Point { if (!array_key_exists($node->getName(), self::$typeMapper)) { return null; @@ -120,9 +120,10 @@ public static function parse(\SimpleXMLElement $node) break; default: if (!in_array($attribute['type'], ['object', 'array'])) { - $point->{$attribute['name']} = isset($node->$key) ? $node->$key : null; - if (!is_null($point->{$attribute['name']})) { - settype($point->{$attribute['name']}, $attribute['type']); + if (isset($node->$key)) { + $value = (string) $node->$key; + settype($value, $attribute['type']); + $point->{$attribute['name']} = $value; } } break; @@ -137,12 +138,16 @@ public static function parse(\SimpleXMLElement $node) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Point $point, \DOMDocument &$document) + public static function toXML(Point $point, \DOMDocument &$document): \DOMElement { $node = $document->createElement(array_search($point->getPointType(), self::$typeMapper)); - $node->setAttribute('lat', $point->latitude); - $node->setAttribute('lon', $point->longitude); + if ($point->latitude !== null) { + $node->setAttribute('lat', $point->latitude); + } + if ($point->longitude !== null) { + $node->setAttribute('lon', $point->longitude); + } foreach (self::$attributeMapper as $key => $attribute) { if (!is_null($point->{$attribute['name']})) { @@ -161,7 +166,6 @@ public static function toXML(Point $point, \DOMDocument &$document) $elementText = $document->createTextNode((string) $point->{$attribute['name']}); $child->appendChild($elementText); break; - break; } if (is_array($child)) { @@ -182,7 +186,7 @@ public static function toXML(Point $point, \DOMDocument &$document) * @param \DOMDocument $document * @return \DOMElement[] */ - public static function toXMLArray(array $points, \DOMDocument &$document) + public static function toXMLArray(array $points, \DOMDocument &$document): array { $result = []; diff --git a/src/phpGPX/Parsers/RouteParser.php b/src/phpGPX/Parsers/RouteParser.php index 5558cd1..aa8982c 100644 --- a/src/phpGPX/Parsers/RouteParser.php +++ b/src/phpGPX/Parsers/RouteParser.php @@ -86,9 +86,10 @@ public static function parse($nodes) break; default: if (!in_array($attribute['type'], ['object', 'array'])) { - $route->{$attribute['name']} = isset($node->$key) ? $node->$key : null; - if (!is_null($route->{$attribute['name']})) { - settype($route->{$attribute['name']}, $attribute['type']); + if (isset($node->$key)) { + $value = (string) $node->$key; + settype($value, $attribute['type']); + $route->{$attribute['name']} = $value; } } break; diff --git a/src/phpGPX/Parsers/TrackParser.php b/src/phpGPX/Parsers/TrackParser.php index 650c17e..8a77a67 100644 --- a/src/phpGPX/Parsers/TrackParser.php +++ b/src/phpGPX/Parsers/TrackParser.php @@ -80,9 +80,10 @@ public static function parse(\SimpleXMLElement $nodes) break; default: if (!in_array($attribute['type'], ['object', 'array'])) { - $track->{$attribute['name']} = isset($node->$key) ? $node->$key : null; - if (!is_null($track->{$attribute['name']})) { - settype($track->{$attribute['name']}, $attribute['type']); + if (isset($node->$key)) { + $value = (string) $node->$key; + settype($value, $attribute['type']); + $track->{$attribute['name']} = $value; } } break; diff --git a/src/phpGPX/Tests/LoadFileTest.php b/src/phpGPX/Tests/LoadFileTest.php deleted file mode 100644 index 807ca71..0000000 --- a/src/phpGPX/Tests/LoadFileTest.php +++ /dev/null @@ -1,228 +0,0 @@ -load($file); - - $this->assertEqualsWithDelta($this->createExpectedArray(), $gpxFile->toArray(), 0.1); - - // Check XML generation - $gpxFile->toXML()->saveXML(); - } - - private function createExpectedArray() - { - return [ - 'waypoints' => [ - [ - 'lat' => 49.3636333333086, - 'lon' => 0.0800866666666667, - 'time' => '2014-12-13T16:32:51+00:00', - 'name' => 'Event 0000', - 'cmt' => '', - 'extensions' => [ - 'unsupported' => [ - 'MxTimeZeroSymbol' => 10, - 'color' => -16744448, - ], - ], - ], - [ - 'lat' => 49.3636333333086, - 'lon' => 0.0800866666666667, - 'time' => '2014-12-13T16:32:52+00:00', - 'name' => 'Event 0001', - 'cmt' => '', - 'extensions' => [ - 'unsupported' => [ - 'MxTimeZeroSymbol' => 10, - 'color' => -16744448, - ], - ], - ], - ], - 'tracks' => [ - [ - 'name' => 'Ownship', - 'extensions' => [ - 'unsupported' => [ - 'guid' => 201, - ], - ], - 'trkseg' => [ - [ - 'points' => [ - [ - 'lat' => 49.3635449998312, - 'lon' => 0.0801483333364938, - 'time' => '2010-01-01T14:48:37+00:00', - ], - [ - 'lat' => 49.3635350651798, - 'lon' => 0.0801416666698513, - 'time' => '2010-01-01T14:48:40+00:00', - 'difference' => 1.2055693602077022, - 'distance' => 1.2055693602077022, - ], - [ - 'lat' => 49.3635266991555, - 'lon' => 0.0801333333365323, - 'time' => '2010-01-01T14:48:46+00:00', - 'difference' => 1.1088552014759407, - 'distance' => 2.314424561683643, - ], - ], - 'stats' => [ - 'distance' => 2.314424561683643, - 'realDistance' => 2.314424561683643, - 'avgSpeed' => 0.2571582846315159, - 'avgPace' => 3888.6555859279733, - 'minAltitude' => 0.0, - 'minAltitudeCoords' => [ - 'lat' => 49.3635449998312, - 'lng' => 0.0801483333364938 - ], - 'maxAltitude' => 0.0, - 'cumulativeElevationGain' => 0.0, - 'cumulativeElevationLoss' => 0.0, - 'startedAt' => '2010-01-01T14:48:37+00:00', - 'startedAtCoords' => [ - 'lat' => 49.3635449998312, - 'lng' => 0.0801483333364938 - ], - 'finishedAt' => '2010-01-01T14:48:46+00:00', - 'finishedAtCoords' => [ - 'lat' => 49.3635266991555, - 'lng' => 0.0801333333365323 - ], - 'duration' => 9.0, - ], - ], - ], - 'stats' => [ - 'distance' => 2.314424561683643, - 'realDistance' => 2.314424561683643, - 'avgSpeed' => 0.2571582846315159, - 'avgPace' => 3888.6555859279733, - 'minAltitude' => 0.0, - 'minAltitudeCoords' => [ - 'lat' => 49.3635449998312, - 'lng' => 0.0801483333364938 - ], - 'maxAltitude' => 0.0, - 'cumulativeElevationGain' => 0.0, - 'cumulativeElevationLoss' => 0.0, - 'startedAt' => '2010-01-01T14:48:37+00:00', - 'startedAtCoords' => [ - 'lat' => 49.3635449998312, - 'lng' => 0.0801483333364938 - ], - 'finishedAt' => '2010-01-01T14:48:46+00:00', - 'finishedAtCoords' => [ - 'lat' => 49.3635266991555, - 'lng' => 0.0801333333365323 - ], - 'duration' => 9.0, - ], - ], - [ - 'name' => 'Ownship', - 'extensions' => [ - 'unsupported' => [ - 'guid' => 102, - ], - ], - 'trkseg' => [ - [ - 'points' => [ - [ - 'lat' => 49.4574117319429, - 'lon' => 0.0343682156842231, - 'time' => '2016-04-03T14:13:09+00:00', - ], - [ - 'lat' => 49.4573966992346, - 'lon' => 0.0343466078409025, - 'time' => '2016-04-03T14:13:10+00:00', - 'difference' => 2.2876315307770505, - 'distance' => 2.2876315307770505, - ], - [ - 'lat' => 49.4573700325059, - 'lon' => 0.0342948235267376, - 'time' => '2016-04-03T14:13:12+00:00', - 'difference' => 4.775098771720203, - 'distance' => 7.062730302497254, - ], - ], - 'stats' => [ - 'distance' => 7.062730302497254, - 'realDistance' => 7.062730302497254, - 'avgSpeed' => 2.354243434165751, - 'avgPace' => 424.7649098167112, - 'minAltitude' => 0.0, - 'minAltitudeCoords' => [ - 'lat' => 49.4574117319429, - 'lng' => 0.0343682156842231 - ], - 'maxAltitude' => 0.0, - 'cumulativeElevationGain' => 0.0, - 'cumulativeElevationLoss' => 0.0, - 'startedAt' => '2016-04-03T14:13:09+00:00', - 'startedAtCoords' => [ - 'lat' => 49.4574117319429, - 'lng' => 0.0343682156842231 - ], - 'finishedAt' => '2016-04-03T14:13:12+00:00', - 'finishedAtCoords' => [ - 'lat' => 49.4573700325059, - 'lng' => 0.0342948235267376 - ], - 'duration' => 3.0, - ], - ], - ], - 'stats' => [ - 'distance' => 7.062730302497254, - 'realDistance' => 7.062730302497254, - 'avgSpeed' => 2.354243434165751, - 'avgPace' => 424.7649098167112, - 'minAltitude' => 0.0, - 'minAltitudeCoords' => [ - 'lat' => 49.4574117319429, - 'lng' => 0.0343682156842231 - ], - 'maxAltitude' => 0.0, - 'cumulativeElevationGain' => 0.0, - 'cumulativeElevationLoss' => 0.0, - 'startedAt' => '2016-04-03T14:13:09+00:00', - 'startedAtCoords' => [ - 'lat' => 49.4574117319429, - 'lng' => 0.0343682156842231 - ], - 'finishedAt' => '2016-04-03T14:13:12+00:00', - 'finishedAtCoords' => [ - 'lat' => 49.4573700325059, - 'lng' => 0.0342948235267376 - ], - 'duration' => 3.0, - ], - ], - ], - ]; - } -} diff --git a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php b/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php deleted file mode 100644 index 2d43f83..0000000 --- a/src/phpGPX/Tests/Parsers/Bounds/BoundsParserTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ - -namespace phpGPX\Tests\Parsers\Bounds; - -use phpGPX\Models\Bounds; -use phpGPX\Parsers\BoundsParser; -use PHPUnit\Framework\TestCase; - -class BoundsParserTest extends TestCase -{ - protected Bounds $bounds; - protected \SimpleXMLElement $file; - - protected function setUp(): void - { - // Example object - $this->bounds = new Bounds( - 49.072489, - 18.814543, - 49.090543, - 18.886939 - ); - - // Input file - $this->file = simplexml_load_file(sprintf("%s/bounds.xml", __DIR__)); - } - - /** - * @covers \phpGPX\Parsers\BoundsParser - * @covers \phpGPX\Models\Bounds - * @return void - */ - public function testParse() - { - $bounds = BoundsParser::parse($this->file->bounds); - - $this->assertEquals($bounds, $this->bounds); - $this->assertNotEmpty($bounds); - - $this->assertEquals($this->bounds->maxLatitude, $bounds->maxLatitude); - $this->assertEquals($this->bounds->maxLongitude, $bounds->maxLongitude); - $this->assertEquals($this->bounds->minLatitude, $bounds->minLatitude); - $this->assertEquals($this->bounds->minLongitude, $bounds->minLongitude); - - $this->assertEquals($this->bounds->jsonSerialize(), $bounds->jsonSerialize()); - } - - /** - * @covers \phpGPX\Parsers\BoundsParser - * @covers \phpGPX\Models\Bounds - * @return void - * @throws \DOMException - */ - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild(BoundsParser::toXML($this->bounds, $document)); - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); - } -} diff --git a/src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php b/src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php deleted file mode 100644 index bf393a3..0000000 --- a/src/phpGPX/Tests/Parsers/Copyright/CopyrightParserTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ - -namespace phpGPX\Tests\Parsers\Copyright; - -use phpGPX\Models\Copyright; -use phpGPX\Parsers\CopyrightParser; -use PHPUnit\Framework\TestCase; - -class CopyrightParserTest extends TestCase -{ - protected Copyright $copyright; - protected \SimpleXMLElement $file; - - protected function setUp(): void - { - $this->copyright = new Copyright(); - $this->copyright->author = "Jakub Dubec"; - $this->copyright->license = "https://github.com/Sibyx/phpGPX/blob/master/LICENSE"; - $this->copyright->year = '2017'; - - // Input file - $this->file = simplexml_load_file(sprintf("%s/copyright.xml", __DIR__)); - } - - /** - * @covers \phpGPX\Parsers\CopyrightParser - * @covers \phpGPX\Helpers\SerializationHelper - * @covers \phpGPX\Models\Copyright - * @return void - */ - public function testParse() - { - $copyright = CopyrightParser::parse($this->file->copyright); - - $this->assertEquals($this->copyright, $copyright); - $this->assertNotEmpty($copyright); - - $this->assertEquals($this->copyright->author, $copyright->author); - $this->assertEquals($this->copyright->license, $copyright->license); - $this->assertEquals($this->copyright->year, $copyright->year); - - $this->assertEquals($this->copyright->toArray(), $copyright->toArray()); - } - - /** - * @covers \phpGPX\Parsers\CopyrightParser - * @covers \phpGPX\Models\Copyright - * @return void - * @throws \DOMException - */ - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild(CopyrightParser::toXML($this->copyright, $document)); - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); - } - - /** - * @covers \phpGPX\Models\Copyright - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testToJSON() - { - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/copyright.json", __DIR__), json_encode($this->copyright->toArray()) - ); - } -} diff --git a/src/phpGPX/Tests/Parsers/Email/EmailParserTest.php b/src/phpGPX/Tests/Parsers/Email/EmailParserTest.php deleted file mode 100644 index c421dcc..0000000 --- a/src/phpGPX/Tests/Parsers/Email/EmailParserTest.php +++ /dev/null @@ -1,73 +0,0 @@ - - */ - -namespace phpGPX\Tests\Parsers\Email; - -use phpGPX\Models\Email; -use phpGPX\Parsers\EmailParser; -use PHPUnit\Framework\TestCase; - -class EmailParserTest extends TestCase -{ - protected Email $email; - protected \SimpleXMLElement $file; - - protected function setUp(): void - { - $this->email = new Email(); - $this->email->id = "jakub.dubec"; - $this->email->domain = "gmail.com"; - - $this->file = simplexml_load_file(sprintf("%s/email.xml", __DIR__)); - } - - /** - * @covers \phpGPX\Parsers\EmailParser - * @covers \phpGPX\Models\Email - * @return void - */ - public function testParse() - { - $email = EmailParser::parse($this->file->email); - - $this->assertEquals($this->email, $email); - $this->assertNotEmpty($email); - - $this->assertEquals($this->email->id, $email->id); - $this->assertEquals($this->email->domain, $email->domain); - - $this->assertEquals($this->email->toArray(), $email->toArray()); - } - - - /** - * @covers \phpGPX\Parsers\EmailParser - * @covers \phpGPX\Models\Email - * @return void - * @throws \DOMException - */ - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild(EmailParser::toXML($this->email, $document)); - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); - } - - /** - * @covers \phpGPX\Models\Email - * @return void - */ - public function testToJSON() - { - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/email.json", __DIR__), json_encode($this->email->toArray()) - ); - } -} diff --git a/src/phpGPX/Tests/Parsers/Link/LinkParserTest.php b/src/phpGPX/Tests/Parsers/Link/LinkParserTest.php deleted file mode 100644 index 7fa728c..0000000 --- a/src/phpGPX/Tests/Parsers/Link/LinkParserTest.php +++ /dev/null @@ -1,75 +0,0 @@ - - */ - -namespace phpGPX\Tests\Parsers\Link; - -use phpGPX\Models\Link; -use phpGPX\Parsers\LinkParser; -use PHPUnit\Framework\TestCase; - -class LinkParserTest extends TestCase -{ - protected Link $link; - protected \SimpleXMLElement $file; - - protected function setUp(): void - { - $this->link = new Link(); - $this->link->href = "https://jakubdubec.me"; - $this->link->text = "Portfolio"; - $this->link->type = "text/html"; - - $this->file = simplexml_load_file(sprintf("%s/link.xml", __DIR__)); - } - - /** - * @covers \phpGPX\Parsers\LinkParser - * @covers \phpGPX\Models\Link - * @return void - */ - public function testParse() - { - $links = LinkParser::parse($this->file->link); - - $this->assertNotEmpty($links); - $this->assertEquals($this->link, $links[0]); - - $this->assertEquals($this->link->href, $links[0]->href); - $this->assertEquals($this->link->text, $links[0]->text); - $this->assertEquals($this->link->type, $links[0]->type); - - $this->assertEquals($this->link->toArray(), $links[0]->toArray()); - } - - - /** - * @covers \phpGPX\Parsers\LinkParser - * @covers \phpGPX\Models\Link - * @return void - * @throws \DOMException - */ - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild(LinkParser::toXML($this->link, $document)); - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); - } - - /** - * @covers \phpGPX\Models\Link - * @return void - */ - public function testToJSON() - { - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/link.json", __DIR__), json_encode($this->link->toArray()) - ); - } -} diff --git a/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php b/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php deleted file mode 100644 index 3e02d13..0000000 --- a/src/phpGPX/Tests/Parsers/Person/PersonParserTest.php +++ /dev/null @@ -1,126 +0,0 @@ - - */ - -namespace phpGPX\Tests\Parsers\Person; - -use phpGPX\Models\Email; -use phpGPX\Models\GpxFile; -use phpGPX\Models\Link; -use phpGPX\Models\Metadata; -use phpGPX\Models\Person; -use phpGPX\Parsers\PersonParser; -use PHPUnit\Framework\TestCase; - -class PersonParserTest extends TestCase -{ - protected Person $person; - protected \SimpleXMLElement $file; - - protected function setUp(): void - { - $this->person = new Person(); - $this->person->name = "Jakub Dubec"; - - $email = new Email(); - $email->id = "jakub.dubec"; - $email->domain = "gmail.com"; - $this->person->email = $email; - - $link = new Link(); - $link->href = "https://jakubdubec.me"; - $link->text = "Portfolio"; - $link->type = "text/html"; - $this->person->links[] = $link; - - $this->file = simplexml_load_file(sprintf("%s/person.xml", __DIR__)); - } - - /** - * @covers \phpGPX\Models\Person - * @covers \phpGPX\Models\Link - * @covers \phpGPX\Models\Email - * @covers \phpGPX\Parsers\EmailParser - * @covers \phpGPX\Parsers\LinkParser - * @covers \phpGPX\Parsers\PersonParser - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testParse() - { - $person = PersonParser::parse($this->file->author); - - $this->assertNotEmpty($person); - - // Primitive attributes - $this->assertEquals($this->person->name, $person->name); - $this->assertEquals($this->person, $person); - - // Email - $this->assertEquals($this->person->email->id, $person->email->id); - $this->assertEquals($this->person->email->domain, $person->email->domain); - - // Link - $this->assertEquals($this->person->links[0]->type, $person->links[0]->type); - $this->assertEquals($this->person->links[0]->text, $person->links[0]->text); - $this->assertEquals($this->person->links[0]->href, $person->links[0]->href); - - // toArray functions - $this->assertEquals($this->person->toArray(), $person->toArray()); - $this->assertEquals($this->person->email->toArray(), $person->email->toArray()); - $this->assertEquals($this->person->links[0]->toArray(), $person->links[0]->toArray()); - } - - /** - * @coversNothing - * @url https://github.com/Sibyx/phpGPX/issues/48 - */ - public function testEmptyLinks() - { - $gpx_file = new GpxFile(); - - $gpx_file->metadata = new Metadata(); - $gpx_file->metadata->author = new Person(); - $gpx_file->metadata->author->name = "Arthur Dent"; - - $this->assertNotNull($gpx_file->toXML()->saveXML()); - } - - - /** - * @covers \phpGPX\Models\Email - * @covers \phpGPX\Models\Link - * @covers \phpGPX\Models\Person - * @covers \phpGPX\Parsers\EmailParser - * @covers \phpGPX\Parsers\LinkParser - * @covers \phpGPX\Parsers\PersonParser - * @return void - * @throws \DOMException - */ - public function testToXML() - { - $document = new \DOMDocument("1.0", 'UTF-8'); - - $root = $document->createElement("document"); - $root->appendChild(PersonParser::toXML($this->person, $document)); - - $document->appendChild($root); - - $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); - } - - /** - * @covers \phpGPX\Models\Person - * @covers \phpGPX\Models\Email - * @covers \phpGPX\Models\Link - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testToJSON() - { - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/person.json", __DIR__), json_encode($this->person->toArray()) - ); - } -} diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index 44d06f5..4bb505e 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -18,12 +18,79 @@ */ class phpGPX { - const JSON_FORMAT = 'json'; + const JSON_FORMAT = 'json'; const XML_FORMAT = 'xml'; + const GEOJSON_FORMAT = 'geojson'; const PACKAGE_NAME = 'phpGPX'; const VERSION = '2.0.0-alpha.1'; + /** + * Pretty print XML output + * @var bool + */ + public static bool $PRETTY_PRINT = true; + + /** + * Ignore elevation values of 0 + * @var bool + */ + public static bool $IGNORE_ELEVATION_0 = false; + + /** + * Calculate stats for tracks, segments and routes + * @var bool + */ + public static bool $CALCULATE_STATS = true; + + /** + * DateTime format for output + * @var string + */ + public static string $DATETIME_FORMAT = 'c'; + + /** + * DateTime timezone output + * @var string|null + */ + public static ?string $DATETIME_TIMEZONE_OUTPUT = null; + + /** + * Additional sort based on timestamp in Routes & Tracks on XML read. + * @var bool + */ + public static bool $SORT_BY_TIMESTAMP = false; + + /** + * Apply elevation gain/loss smoothing + * @var bool + */ + public static bool $APPLY_ELEVATION_SMOOTHING = false; + + /** + * Minimum elevation difference threshold in meters for smoothing + * @var int + */ + public static int $ELEVATION_SMOOTHING_THRESHOLD = 2; + + /** + * Maximum elevation difference threshold in meters for spike filtering + * @var int|null + */ + public static ?int $ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; + + /** + * Apply distance calculation smoothing + * @var bool + */ + public static bool $APPLY_DISTANCE_SMOOTHING = false; + + /** + * Minimum distance threshold in meters for smoothing + * @var int + */ + public static int $DISTANCE_SMOOTHING_THRESHOLD = 2; + /** * Load GPX file. * @param string $path diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php new file mode 100644 index 0000000..ebd3f56 --- /dev/null +++ b/tests/Integration/GeoJsonOutputTest.php @@ -0,0 +1,169 @@ +jsonSerialize(); + + $this->assertEquals('FeatureCollection', $json['type']); + $this->assertArrayHasKey('features', $json); + $this->assertIsArray($json['features']); + } + + public function testWaypointJsonIsPointFeature(): void + { + $point = new Point(Point::WAYPOINT); + $point->latitude = 49.363; + $point->longitude = 0.080; + $point->elevation = 100.0; + $point->name = 'Test Waypoint'; + + $json = $point->jsonSerialize(); + + $this->assertEquals('Feature', $json['type']); + $this->assertEquals('Point', $json['geometry']['type']); + $this->assertCount(3, $json['geometry']['coordinates']); + $this->assertEqualsWithDelta(0.080, $json['geometry']['coordinates'][0], 0.001); + $this->assertEqualsWithDelta(49.363, $json['geometry']['coordinates'][1], 0.001); + $this->assertEqualsWithDelta(100.0, $json['geometry']['coordinates'][2], 0.001); + $this->assertEquals('Test Waypoint', $json['properties']['name']); + } + + public function testRouteJsonIsLineStringFeature(): void + { + $route = new Route(); + $route->name = 'Test Route'; + + $p1 = new Point(Point::ROUTEPOINT); + $p1->latitude = 54.932; + $p1->longitude = 9.860; + $p1->elevation = 0.0; + + $p2 = new Point(Point::ROUTEPOINT); + $p2->latitude = 54.933; + $p2->longitude = 9.861; + $p2->elevation = 1.0; + + $route->points = [$p1, $p2]; + $route->recalculateStats(); + + $json = $route->jsonSerialize(); + + $this->assertEquals('Feature', $json['type']); + $this->assertEquals('LineString', $json['geometry']['type']); + $this->assertCount(2, $json['geometry']['coordinates']); + + // GeoJSON uses [lon, lat, ele] + $this->assertEqualsWithDelta(9.860, $json['geometry']['coordinates'][0][0], 0.001); + $this->assertEqualsWithDelta(54.932, $json['geometry']['coordinates'][0][1], 0.001); + $this->assertEquals('Test Route', $json['properties']['name']); + } + + public function testTrackJsonIsMultiLineStringFeature(): void + { + $track = new Track(); + $track->name = 'Test Track'; + + $seg1 = new Segment(); + $p1 = new Point(Point::TRACKPOINT); + $p1->latitude = 46.571; + $p1->longitude = 8.414; + $p1->elevation = 2419.0; + + $p2 = new Point(Point::TRACKPOINT); + $p2->latitude = 46.572; + $p2->longitude = 8.415; + $p2->elevation = 2420.0; + $seg1->points = [$p1, $p2]; + + $seg2 = new Segment(); + $p3 = new Point(Point::TRACKPOINT); + $p3->latitude = 46.573; + $p3->longitude = 8.416; + $p3->elevation = 2421.0; + $seg2->points = [$p3]; + + $track->segments = [$seg1, $seg2]; + $track->recalculateStats(); + + $json = $track->jsonSerialize(); + + $this->assertEquals('Feature', $json['type']); + $this->assertEquals('MultiLineString', $json['geometry']['type']); + $this->assertCount(2, $json['geometry']['coordinates']); + $this->assertCount(2, $json['geometry']['coordinates'][0]); // seg1 has 2 points + $this->assertCount(1, $json['geometry']['coordinates'][1]); // seg2 has 1 point + $this->assertEquals('Test Track', $json['properties']['name']); + } + + public function testLoadedFileGeoJsonStructure(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); + $json = $gpxFile->jsonSerialize(); + + $this->assertEquals('FeatureCollection', $json['type']); + + // Should have features: 1 route + 1 track = 2 features (no waypoints in this file) + $this->assertCount(2, $json['features']); + + // Route feature + $routeFeature = $json['features'][0]; + $this->assertEquals('Feature', $routeFeature['type']); + $this->assertEquals('LineString', $routeFeature['geometry']['type']); + + // Track feature + $trackFeature = $json['features'][1]; + $this->assertEquals('Feature', $trackFeature['type']); + $this->assertEquals('MultiLineString', $trackFeature['geometry']['type']); + } + + public function testGeoJsonWithWaypoints(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/timezero.gpx'); + $json = $gpxFile->jsonSerialize(); + + $this->assertEquals('FeatureCollection', $json['type']); + + // 2 waypoints + 2 tracks = 4 features + $this->assertCount(4, $json['features']); + + // First two should be waypoint Point features + $this->assertEquals('Point', $json['features'][0]['geometry']['type']); + $this->assertEquals('Point', $json['features'][1]['geometry']['type']); + + // Last two should be track MultiLineString features + $this->assertEquals('MultiLineString', $json['features'][2]['geometry']['type']); + $this->assertEquals('MultiLineString', $json['features'][3]['geometry']['type']); + } + + public function testToJsonOutput(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + + // GeoJSON format + $geoJson = $gpxFile->toJSON(true); + $decoded = json_decode($geoJson, true); + $this->assertNotNull($decoded); + $this->assertEquals('FeatureCollection', $decoded['type']); + + // GPX array format + $gpxJson = $gpxFile->toJSON(false); + $decoded = json_decode($gpxJson, true); + $this->assertNotNull($decoded); + $this->assertArrayHasKey('routes', $decoded); + } +} \ No newline at end of file diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php new file mode 100644 index 0000000..198b3e6 --- /dev/null +++ b/tests/Integration/GpxFileLoadTest.php @@ -0,0 +1,145 @@ +assertCount(2, $gpxFile->waypoints); + $this->assertEquals('Event 0000', $gpxFile->waypoints[0]->name); + $this->assertEquals('Event 0001', $gpxFile->waypoints[1]->name); + $this->assertEqualsWithDelta(49.3636333333086, $gpxFile->waypoints[0]->latitude, 0.0001); + + // Waypoint extensions (unsupported) + $this->assertNotNull($gpxFile->waypoints[0]->extensions); + $this->assertArrayHasKey('MxTimeZeroSymbol', $gpxFile->waypoints[0]->extensions->unsupported); + + // Tracks + $this->assertCount(2, $gpxFile->tracks); + $this->assertEquals('Ownship', $gpxFile->tracks[0]->name); + + // Track segments + $this->assertCount(1, $gpxFile->tracks[0]->segments); + $this->assertCount(3, $gpxFile->tracks[0]->segments[0]->points); + + // Track stats + $this->assertNotNull($gpxFile->tracks[0]->stats); + $this->assertGreaterThan(0, $gpxFile->tracks[0]->stats->distance); + $this->assertEqualsWithDelta(2.31, $gpxFile->tracks[0]->stats->distance, 0.1); + $this->assertEqualsWithDelta(9.0, $gpxFile->tracks[0]->stats->duration, 0.1); + + // Second track + $this->assertCount(3, $gpxFile->tracks[1]->segments[0]->points); + $this->assertEqualsWithDelta(7.06, $gpxFile->tracks[1]->stats->distance, 0.1); + $this->assertEqualsWithDelta(3.0, $gpxFile->tracks[1]->stats->duration, 0.1); + + // XML generation should not throw + $xml = $gpxFile->toXML()->saveXML(); + $this->assertNotEmpty($xml); + } + + public function testLoadRouteGpx(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + + $this->assertEmpty($gpxFile->tracks); + $this->assertEmpty($gpxFile->waypoints); + $this->assertCount(2, $gpxFile->routes); + + $route1 = $gpxFile->routes[0]; + $this->assertEquals("Patrick's Route", $route1->name); + $this->assertCount(4, $route1->points); + + // Route point coordinates + $this->assertEqualsWithDelta(54.9328621088893, $route1->points[0]->latitude, 0.0001); + $this->assertEqualsWithDelta(9.860624216140083, $route1->points[0]->longitude, 0.0001); + + // Route stats + $this->assertNotNull($route1->stats); + $this->assertGreaterThan(0, $route1->stats->distance); + $this->assertEqualsWithDelta(0.0, $route1->stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(3.0, $route1->stats->maxAltitude, 0.01); + + // Second route + $route2 = $gpxFile->routes[1]; + $this->assertEquals("Sibyx's Route", $route2->name); + $this->assertCount(4, $route2->points); + } + + public function testLoadGpsTrackGpx(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/gps-track.gpx'); + + $this->assertCount(1, $gpxFile->tracks); + $this->assertEquals('GPS-Track', $gpxFile->tracks[0]->name); + + // First segment has 5 points, second segment is empty + $track = $gpxFile->tracks[0]; + $this->assertGreaterThanOrEqual(1, count($track->segments)); + + $segment = $track->segments[0]; + $this->assertCount(5, $segment->points); + + // Elevation data + $this->assertEqualsWithDelta(2419, $segment->points[0]->elevation, 0.01); + $this->assertEqualsWithDelta(2425, $segment->points[4]->elevation, 0.01); + + // Stats + $this->assertNotNull($segment->stats); + $this->assertEqualsWithDelta(2418.88, $segment->stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(2425, $segment->stats->maxAltitude, 0.01); + $this->assertGreaterThan(0, $segment->stats->cumulativeElevationGain); + } + + public function testLoadMinimalGpx(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); + + // Has metadata + $this->assertNotNull($gpxFile->metadata); + $this->assertEquals('Minimal GPX Scenario', $gpxFile->metadata->name); + $this->assertNotNull($gpxFile->metadata->author); + $this->assertEquals('Jakub Dubec', $gpxFile->metadata->author->name); + + // Has route + $this->assertCount(1, $gpxFile->routes); + $this->assertEquals("Patrick's Route", $gpxFile->routes[0]->name); + $this->assertCount(4, $gpxFile->routes[0]->points); + + // Has track with heart rate extensions + $this->assertCount(1, $gpxFile->tracks); + $this->assertEquals('Hike', $gpxFile->tracks[0]->name); + $this->assertEquals('hiking', $gpxFile->tracks[0]->type); + $this->assertCount(2, $gpxFile->tracks[0]->segments); + + // Check TrackPointExtension (heart rate) + $firstPoint = $gpxFile->tracks[0]->segments[0]->points[0]; + $this->assertNotNull($firstPoint->extensions); + $this->assertNotNull($firstPoint->extensions->trackPointExtension); + $this->assertEqualsWithDelta(126, $firstPoint->extensions->trackPointExtension->hr, 0.1); + } + + public function testLoadCreatorAttribute(): void + { + $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $this->assertEquals('RouteConverter', $gpxFile->creator); + } + + public function testParseFromString(): void + { + $xml = file_get_contents(self::FIXTURES_DIR . '/route.gpx'); + $gpxFile = phpGPX::parse($xml); + + $this->assertCount(2, $gpxFile->routes); + $this->assertEquals("Patrick's Route", $gpxFile->routes[0]->name); + } +} \ No newline at end of file diff --git a/tests/Integration/XmlRoundTripTest.php b/tests/Integration/XmlRoundTripTest.php new file mode 100644 index 0000000..4053c5c --- /dev/null +++ b/tests/Integration/XmlRoundTripTest.php @@ -0,0 +1,147 @@ +toXML()->saveXML(); + $reloaded = phpGPX::parse($xml); + + $this->assertCount(count($original->waypoints), $reloaded->waypoints); + $this->assertCount(count($original->tracks), $reloaded->tracks); + + // Verify waypoint data survives round-trip + for ($i = 0; $i < count($original->waypoints); $i++) { + $this->assertEqualsWithDelta( + $original->waypoints[$i]->latitude, + $reloaded->waypoints[$i]->latitude, + 0.0001 + ); + $this->assertEquals($original->waypoints[$i]->name, $reloaded->waypoints[$i]->name); + } + + // Verify track structure survives round-trip + for ($t = 0; $t < count($original->tracks); $t++) { + $this->assertEquals($original->tracks[$t]->name, $reloaded->tracks[$t]->name); + $this->assertCount( + count($original->tracks[$t]->segments), + $reloaded->tracks[$t]->segments + ); + + for ($s = 0; $s < count($original->tracks[$t]->segments); $s++) { + $this->assertCount( + count($original->tracks[$t]->segments[$s]->points), + $reloaded->tracks[$t]->segments[$s]->points + ); + } + } + } + + public function testRoundTripRoute(): void + { + $original = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $xml = $original->toXML()->saveXML(); + $reloaded = phpGPX::parse($xml); + + $this->assertCount(count($original->routes), $reloaded->routes); + + for ($r = 0; $r < count($original->routes); $r++) { + $this->assertEquals($original->routes[$r]->name, $reloaded->routes[$r]->name); + $this->assertCount( + count($original->routes[$r]->points), + $reloaded->routes[$r]->points + ); + + for ($p = 0; $p < count($original->routes[$r]->points); $p++) { + $origPoint = $original->routes[$r]->points[$p]; + $reloadedPoint = $reloaded->routes[$r]->points[$p]; + + $this->assertEqualsWithDelta($origPoint->latitude, $reloadedPoint->latitude, 0.0001); + $this->assertEqualsWithDelta($origPoint->longitude, $reloadedPoint->longitude, 0.0001); + $this->assertEquals($origPoint->name, $reloadedPoint->name); + } + } + } + + public function testRoundTripGpsTrack(): void + { + $original = phpGPX::load(self::FIXTURES_DIR . '/gps-track.gpx'); + $xml = $original->toXML()->saveXML(); + $reloaded = phpGPX::parse($xml); + + $this->assertCount(1, $reloaded->tracks); + $this->assertEquals('GPS-Track', $reloaded->tracks[0]->name); + + $origSeg = $original->tracks[0]->segments[0]; + $reloadedSeg = $reloaded->tracks[0]->segments[0]; + + $this->assertCount(count($origSeg->points), $reloadedSeg->points); + + // Verify elevation survives round-trip + for ($i = 0; $i < count($origSeg->points); $i++) { + $this->assertEqualsWithDelta( + $origSeg->points[$i]->elevation, + $reloadedSeg->points[$i]->elevation, + 0.01 + ); + } + } + + public function testRoundTripMinimalWithExtensions(): void + { + $original = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); + $xml = $original->toXML()->saveXML(); + $reloaded = phpGPX::parse($xml); + + // Metadata survives + $this->assertNotNull($reloaded->metadata); + $this->assertEquals($original->metadata->name, $reloaded->metadata->name); + + // Route survives + $this->assertCount(1, $reloaded->routes); + + // Track with extensions survives + $this->assertCount(1, $reloaded->tracks); + $origPoint = $original->tracks[0]->segments[0]->points[0]; + $reloadedPoint = $reloaded->tracks[0]->segments[0]->points[0]; + + $this->assertNotNull($reloadedPoint->extensions); + $this->assertNotNull($reloadedPoint->extensions->trackPointExtension); + $this->assertEqualsWithDelta( + $origPoint->extensions->trackPointExtension->hr, + $reloadedPoint->extensions->trackPointExtension->hr, + 0.1 + ); + } + + public function testRoundTripStatsConsistency(): void + { + $original = phpGPX::load(self::FIXTURES_DIR . '/gps-track.gpx'); + $xml = $original->toXML()->saveXML(); + $reloaded = phpGPX::parse($xml); + + $origStats = $original->tracks[0]->stats; + $reloadedStats = $reloaded->tracks[0]->stats; + + $this->assertEqualsWithDelta($origStats->distance, $reloadedStats->distance, 0.01); + $this->assertEqualsWithDelta($origStats->duration, $reloadedStats->duration, 0.1); + $this->assertEqualsWithDelta($origStats->minAltitude, $reloadedStats->minAltitude, 0.01); + $this->assertEqualsWithDelta($origStats->maxAltitude, $reloadedStats->maxAltitude, 0.01); + $this->assertEqualsWithDelta( + $origStats->cumulativeElevationGain, + $reloadedStats->cumulativeElevationGain, + 0.01 + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Helpers/DateTimeHelperTest.php b/tests/Unit/Helpers/DateTimeHelperTest.php new file mode 100644 index 0000000..ea53278 --- /dev/null +++ b/tests/Unit/Helpers/DateTimeHelperTest.php @@ -0,0 +1,56 @@ +time = $time1; + + $point2 = new Point(Point::WAYPOINT); + $time2 = new \DateTime("2017-08-12T20:15:19+00:00", new \DateTimeZone("UTC")); + $point2->time = $time2; + + $this->assertTrue(($time1 > $time2) && DateTimeHelper::comparePointsByTimestamp($point1, $point2)); + } + + public function testFormatDateTime(): void + { + $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); + + $this->assertEquals( + $datetime->format("Y-m-d H:i:s"), + DateTimeHelper::formatDateTime($datetime, "Y-m-d H:i:s") + ); + + $this->assertNull(DateTimeHelper::formatDateTime(null), "NULL input"); + $this->assertNull(DateTimeHelper::formatDateTime(""), "Empty string input"); + + $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); + $this->assertEquals( + "2017-08-12 21:16:29", + DateTimeHelper::formatDateTime($datetime, "Y-m-d H:i:s", '+01:00') + ); + } + + public function testParseDateTime(): void + { + $this->assertEquals( + new \DateTime("2017-08-12T20:16:29+00:00"), + DateTimeHelper::parseDateTime("2017-08-12T20:16:29+00:00") + ); + } + + public function testParseDateTimeInvalidInput(): void + { + $this->expectException("Exception"); + DateTimeHelper::parseDateTime("Invalid exception"); + } +} \ No newline at end of file diff --git a/tests/Unit/Helpers/DistanceCalculatorTest.php b/tests/Unit/Helpers/DistanceCalculatorTest.php new file mode 100644 index 0000000..7acf21c --- /dev/null +++ b/tests/Unit/Helpers/DistanceCalculatorTest.php @@ -0,0 +1,137 @@ +latitude = $lat; + $p->longitude = $lon; + $p->elevation = $ele; + return $p; + } + + public function testEmptyPoints(): void + { + $calc = new DistanceCalculator([]); + $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); + $this->assertEqualsWithDelta(0.0, $calc->getRealDistance(), 0.001); + } + + public function testSinglePoint(): void + { + $points = [$this->makePoint(48.157, 17.054)]; + $calc = new DistanceCalculator($points); + $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); + } + + public function testTwoPoints(): void + { + $p1 = $this->makePoint(48.1573923225717, 17.0547121910204, 100); + $p2 = $this->makePoint(48.1644916381763, 17.0591753907502, 200); + + $expectedRaw = GeoHelper::getRawDistance($p1, $p2); + $expectedReal = GeoHelper::getRealDistance($p1, $p2); + + $calc = new DistanceCalculator([$p1, $p2]); + + $this->assertEqualsWithDelta($expectedRaw, $calc->getRawDistance(), 0.01); + $this->assertEqualsWithDelta($expectedReal, $calc->getRealDistance(), 0.01); + } + + public function testMultiplePointsAccumulate(): void + { + // Three points forming a path — distance should be sum of segments + $p1 = $this->makePoint(46.571948, 8.414757, 2419); + $p2 = $this->makePoint(46.572016, 8.414866, 2418.88); + $p3 = $this->makePoint(46.572088, 8.414911, 2419.90); + + $d12 = GeoHelper::getRawDistance($p1, $p2); + $d23 = GeoHelper::getRawDistance($p2, $p3); + + $calc = new DistanceCalculator([$p1, $p2, $p3]); + $totalRaw = $calc->getRawDistance(); + + $this->assertEqualsWithDelta($d12 + $d23, $totalRaw, 0.01); + } + + public function testPointsDifferenceAndDistanceAreSet(): void + { + $p1 = $this->makePoint(46.571948, 8.414757); + $p2 = $this->makePoint(46.572016, 8.414866); + $p3 = $this->makePoint(46.572088, 8.414911); + + $calc = new DistanceCalculator([$p1, $p2, $p3]); + $calc->getRawDistance(); + + // First point should have no difference set + $this->assertNull($p1->difference); + + // Second point should have difference from first + $this->assertNotNull($p2->difference); + $this->assertGreaterThan(0, $p2->difference); + + // Third point distance should be cumulative + $this->assertNotNull($p3->distance); + $this->assertEqualsWithDelta($p2->difference + $p3->difference, $p3->distance, 0.01); + } + + public function testDistanceSmoothingFiltersSmallMovements(): void + { + phpGPX::$APPLY_DISTANCE_SMOOTHING = true; + phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 10; // 10 meter threshold + + // Points very close together (< 10m apart) + $p1 = $this->makePoint(46.571948, 8.414757); + $p2 = $this->makePoint(46.571949, 8.414758); // ~0.1m away + $p3 = $this->makePoint(46.571950, 8.414759); // ~0.1m away + + $calc = new DistanceCalculator([$p1, $p2, $p3]); + $distance = $calc->getRawDistance(); + + // With smoothing, these tiny movements should be filtered out + $this->assertEqualsWithDelta(0.0, $distance, 0.01); + + phpGPX::$APPLY_DISTANCE_SMOOTHING = false; + } + + public function testDistanceSmoothingKeepsLargeMovements(): void + { + phpGPX::$APPLY_DISTANCE_SMOOTHING = true; + phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; + + // Points ~857m apart — well above threshold + $p1 = $this->makePoint(48.1573923225717, 17.0547121910204); + $p2 = $this->makePoint(48.1644916381763, 17.0591753907502); + + $calc = new DistanceCalculator([$p1, $p2]); + $distance = $calc->getRawDistance(); + + $this->assertGreaterThan(800, $distance); + + phpGPX::$APPLY_DISTANCE_SMOOTHING = false; + } + + public function testSamePointRepeatedZeroDistance(): void + { + $p1 = $this->makePoint(46.571948, 8.414757); + $p2 = $this->makePoint(46.571948, 8.414757); + $p3 = $this->makePoint(46.571948, 8.414757); + + $calc = new DistanceCalculator([$p1, $p2, $p3]); + $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); + } +} \ No newline at end of file diff --git a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php new file mode 100644 index 0000000..54eb1ab --- /dev/null +++ b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php @@ -0,0 +1,196 @@ +latitude = 46.57; + $p->longitude = 8.41; + $p->elevation = $ele; + return $p; + } + + public function testEmptyPoints(): void + { + [$gain, $loss] = ElevationGainLossCalculator::calculate([]); + $this->assertEqualsWithDelta(0.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testSinglePoint(): void + { + [$gain, $loss] = ElevationGainLossCalculator::calculate([$this->makePoint(100)]); + $this->assertEqualsWithDelta(0.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testFlatTrack(): void + { + $points = [ + $this->makePoint(100), + $this->makePoint(100), + $this->makePoint(100), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(0.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testUphillOnly(): void + { + $points = [ + $this->makePoint(100), + $this->makePoint(150), + $this->makePoint(200), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(100.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testDownhillOnly(): void + { + $points = [ + $this->makePoint(200), + $this->makePoint(150), + $this->makePoint(100), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(0.0, $gain, 0.001); + $this->assertEqualsWithDelta(100.0, $loss, 0.001); + } + + public function testUpAndDown(): void + { + // Up 50, down 30, up 20 + $points = [ + $this->makePoint(100), + $this->makePoint(150), + $this->makePoint(120), + $this->makePoint(140), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(70.0, $gain, 0.001); // 50 + 20 + $this->assertEqualsWithDelta(30.0, $loss, 0.001); + } + + public function testNullElevationSkipped(): void + { + $p1 = $this->makePoint(100); + $p2 = new Point(Point::TRACKPOINT); + $p2->latitude = 46.57; + $p2->longitude = 8.41; + $p2->elevation = null; + $p3 = $this->makePoint(200); + + [$gain, $loss] = ElevationGainLossCalculator::calculate([$p1, $p2, $p3]); + $this->assertEqualsWithDelta(100.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testIgnoreElevationZero(): void + { + phpGPX::$IGNORE_ELEVATION_0 = true; + + $points = [ + $this->makePoint(100), + $this->makePoint(0), // should be skipped + $this->makePoint(200), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(100.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testIgnoreElevationZeroDisabled(): void + { + phpGPX::$IGNORE_ELEVATION_0 = false; + + $points = [ + $this->makePoint(100), + $this->makePoint(0), + $this->makePoint(200), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(200.0, $gain, 0.001); // 0→200 + $this->assertEqualsWithDelta(100.0, $loss, 0.001); // 100→0 + } + + public function testSmoothingFiltersSmallChanges(): void + { + phpGPX::$APPLY_ELEVATION_SMOOTHING = true; + phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 5; + + // Small oscillations of 2m — below 5m threshold, should be filtered + $points = [ + $this->makePoint(100), + $this->makePoint(102), + $this->makePoint(100), + $this->makePoint(102), + $this->makePoint(100), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(0.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testSmoothingKeepsLargeChanges(): void + { + phpGPX::$APPLY_ELEVATION_SMOOTHING = true; + phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 5; + + // Large change of 50m — above 5m threshold + $points = [ + $this->makePoint(100), + $this->makePoint(150), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + $this->assertEqualsWithDelta(50.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } + + public function testSmoothingSpikesThreshold(): void + { + phpGPX::$APPLY_ELEVATION_SMOOTHING = true; + phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; + phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = 50; + + // Spike of 100m — above spikes threshold, should be filtered + $points = [ + $this->makePoint(100), + $this->makePoint(200), // +100m spike, above 50m threshold + $this->makePoint(105), + ]; + + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + // The 100m jump is filtered (> spikes threshold) + // The 200→105 drop: delta from last considered (100) to 200 is 100 (filtered) + // delta from 100 to 105 is 5 (above 2, below 50) — counted + $this->assertEqualsWithDelta(5.0, $gain, 0.001); + $this->assertEqualsWithDelta(0.0, $loss, 0.001); + } +} \ No newline at end of file diff --git a/tests/phpGPX/Helpers/GeoHelperTest.php b/tests/Unit/Helpers/GeoHelperTest.php similarity index 52% rename from tests/phpGPX/Helpers/GeoHelperTest.php rename to tests/Unit/Helpers/GeoHelperTest.php index 64b81d1..2af5ea6 100644 --- a/tests/phpGPX/Helpers/GeoHelperTest.php +++ b/tests/Unit/Helpers/GeoHelperTest.php @@ -1,26 +1,21 @@ - */ -namespace phpGPX\Helpers; +namespace phpGPX\Tests\Unit\Helpers; +use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; use PHPUnit\Framework\TestCase; class GeoHelperTest extends TestCase { - /** * Tested with https://www.freemaptools.com/measure-distance.htm * * Input points: * - 48.1573923225717 17.0547121910204 * - 48.1644916381763 17.0591753907502 - * @covers \phpGPX\Helpers\GeoHelper - * @covers \phpGPX\Models\Point */ - public function testGetDistance() + public function testGetDistance(): void { $point1 = new Point(Point::WAYPOINT); $point1->latitude = 48.1573923225717; @@ -34,16 +29,14 @@ public function testGetDistance() 856.97, GeoHelper::getRawDistance($point1, $point2), 1, - "Invalid distance between two points!" + "Invalid distance between two points!" ); } /** - * @covers \phpGPX\Helpers\GeoHelper - * @covers \phpGPX\Models\Point * @link http://cosinekitty.com/compass.html */ - public function testRealDistance() + public function testRealDistance(): void { $point1 = new Point(Point::WAYPOINT); $point1->latitude = 48.1573923225717; @@ -58,15 +51,44 @@ public function testRealDistance() $this->assertEqualsWithDelta( 856.97, GeoHelper::getRawDistance($point1, $point2), - 1, + 1, "Invalid distance between two points!" ); $this->assertEqualsWithDelta( 862, GeoHelper::getRealDistance($point1, $point2), - 1, + 1, "Invalid real distance between two points!" ); } -} + + public function testSamePointZeroDistance(): void + { + $point1 = new Point(Point::WAYPOINT); + $point1->latitude = 48.1573923225717; + $point1->longitude = 17.0547121910204; + + $point2 = new Point(Point::WAYPOINT); + $point2->latitude = 48.1573923225717; + $point2->longitude = 17.0547121910204; + + $this->assertEqualsWithDelta(0.0, GeoHelper::getRawDistance($point1, $point2), 0.001); + } + + public function testRealDistanceWithNullElevation(): void + { + $point1 = new Point(Point::WAYPOINT); + $point1->latitude = 48.1573923225717; + $point1->longitude = 17.0547121910204; + + $point2 = new Point(Point::WAYPOINT); + $point2->latitude = 48.1644916381763; + $point2->longitude = 17.0591753907502; + + // With null elevation, real distance should equal raw distance + $rawDist = GeoHelper::getRawDistance($point1, $point2); + $realDist = GeoHelper::getRealDistance($point1, $point2); + $this->assertEqualsWithDelta($rawDist, $realDist, 0.001); + } +} \ No newline at end of file diff --git a/tests/Unit/Helpers/SerializationHelperTest.php b/tests/Unit/Helpers/SerializationHelperTest.php new file mode 100644 index 0000000..b125272 --- /dev/null +++ b/tests/Unit/Helpers/SerializationHelperTest.php @@ -0,0 +1,88 @@ +assertNull(SerializationHelper::integerOrNull("")); + $this->assertNull(SerializationHelper::integerOrNull(null)); + $this->assertNull(SerializationHelper::integerOrNull("BLA")); + $this->assertIsInt(SerializationHelper::integerOrNull(5)); + $this->assertIsInt(SerializationHelper::integerOrNull("5")); + } + + public function testFloatOrNull(): void + { + $this->assertNull(SerializationHelper::floatOrNull("")); + $this->assertNull(SerializationHelper::floatOrNull(null)); + $this->assertNull(SerializationHelper::floatOrNull("BLA")); + $this->assertIsFloat(SerializationHelper::floatOrNull(5.6)); + $this->assertIsFloat(SerializationHelper::floatOrNull(5)); + $this->assertIsFloat(SerializationHelper::floatOrNull("5.6")); + $this->assertIsFloat(SerializationHelper::floatOrNull("5")); + } + + public function testStringOrNull(): void + { + $this->assertNull(SerializationHelper::stringOrNull(null)); + $this->assertIsString(SerializationHelper::stringOrNull("")); + $this->assertIsString(SerializationHelper::stringOrNull("Bla bla")); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderFilterNotNull')] + public function testFilterNotNull(array $expected, array $actual): void + { + $this->assertEquals($expected, SerializationHelper::filterNotNull($actual)); + } + + public static function dataProviderFilterNotNull(): array + { + return [ + 'numeric 1' => [ + [], + [null], + ], + 'numeric 2' => [ + [], + [null, [null]], + ], + 'numeric 3' => [ + [1 => 1], + [null, 1], + ], + 'numeric 4' => [ + [1 => 1, 3 => 2], + [null, 1, null, 2, null], + ], + 'numeric 5' => [ + [1 => 1, 3 => 2, 5 => [0 => 3, 2 => 4], 6 => 5], + [null, 1, null, 2, null, [3, null, 4], 5, null], + ], + 'associative 1' => [ + [], + ["foo" => null], + ], + 'associative 2' => [ + [], + ["foo" => null, ["bar" => null]], + ], + 'associative 3' => [ + ["bar" => 1], + ["foo" => null, "bar" => 1], + ], + 'associative 4' => [ + ["bar" => 1, "caw" => 2], + ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null], + ], + 'associative 5' => [ + ["bar" => 1, "caw" => 2, "ere" => ["foo" => 3, "baz" => 4], "moo" => 5], + ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null, "ere" => ["foo" => 3, "bar" => null, "baz" => 4], "moo" => 5, "boo" => null], + ], + ]; + } +} \ No newline at end of file diff --git a/tests/Unit/Models/BoundsTest.php b/tests/Unit/Models/BoundsTest.php new file mode 100644 index 0000000..a06421a --- /dev/null +++ b/tests/Unit/Models/BoundsTest.php @@ -0,0 +1,56 @@ +bounds = new Bounds( + 49.072489, + 18.814543, + 49.090543, + 18.886939 + ); + } + + public function testConstructor(): void + { + $this->assertEquals(49.072489, $this->bounds->minLatitude); + $this->assertEquals(18.814543, $this->bounds->minLongitude); + $this->assertEquals(49.090543, $this->bounds->maxLatitude); + $this->assertEquals(18.886939, $this->bounds->maxLongitude); + } + + public function testJsonSerialize(): void + { + $expected = [18.814543, 49.072489, 18.886939, 49.090543]; + $this->assertEquals($expected, $this->bounds->jsonSerialize()); + } + + public function testParse(): void + { + $xml = new SimpleXMLElement(''); + $bounds = Bounds::parse($xml); + + $this->assertInstanceOf(Bounds::class, $bounds); + $this->assertEquals(49.072489, $bounds->minLatitude); + $this->assertEquals(18.814543, $bounds->minLongitude); + $this->assertEquals(49.090543, $bounds->maxLatitude); + $this->assertEquals(18.886939, $bounds->maxLongitude); + } + + public function testParseInvalidNode(): void + { + $xml = new SimpleXMLElement(''); + $bounds = Bounds::parse($xml); + + $this->assertNull($bounds); + } +} \ No newline at end of file diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php new file mode 100644 index 0000000..aeca7ac --- /dev/null +++ b/tests/Unit/Models/StatsCalculationTest.php @@ -0,0 +1,352 @@ +latitude = $lat; + $p->longitude = $lon; + $p->elevation = $ele; + $p->time = $time ? new \DateTime($time) : null; + return $p; + } + + private function makeRoutePoint( + float $lat, + float $lon, + ?float $ele = null, + ?string $time = null + ): Point { + $p = new Point(Point::ROUTEPOINT); + $p->latitude = $lat; + $p->longitude = $lon; + $p->elevation = $ele; + $p->time = $time ? new \DateTime($time) : null; + return $p; + } + + // --- Segment stats --- + + public function testSegmentStatsEmptyPoints(): void + { + $segment = new Segment(); + $segment->recalculateStats(); + + $this->assertInstanceOf(Stats::class, $segment->stats); + $this->assertNull($segment->stats->distance); + } + + public function testSegmentStatsSinglePoint(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, 2419, '2017-08-13T07:10:41Z'), + ]; + $segment->recalculateStats(); + + $this->assertEqualsWithDelta(0.0, $segment->stats->distance, 0.01); + $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationLoss, 0.01); + $this->assertEquals(46.571948, $segment->stats->startedAtCoords['lat']); + $this->assertEquals(46.571948, $segment->stats->finishedAtCoords['lat']); + } + + public function testSegmentStatsBasicTrack(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, 2419, '2017-08-13T07:10:41Z'), + $this->makePoint(46.572016, 8.414866, 2418.88, '2017-08-13T07:10:54Z'), + $this->makePoint(46.572088, 8.414911, 2419.90, '2017-08-13T07:11:56Z'), + $this->makePoint(46.572069, 8.414912, 2422, '2017-08-13T07:12:15Z'), + $this->makePoint(46.572054, 8.414888, 2425, '2017-08-13T07:12:18Z'), + ]; + $segment->recalculateStats(); + + // Distance should be positive + $this->assertGreaterThan(0, $segment->stats->distance); + $this->assertGreaterThan(0, $segment->stats->realDistance); + + // Elevation gain: 2418.88→2419.90 (+1.02), 2419.90→2422 (+2.1), 2422→2425 (+3) + $this->assertGreaterThan(6, $segment->stats->cumulativeElevationGain); + + // Elevation loss: 2419→2418.88 (-0.12) + $this->assertGreaterThan(0, $segment->stats->cumulativeElevationLoss); + + // Duration + $this->assertEqualsWithDelta(97.0, $segment->stats->duration, 0.1); + + // Speed and pace + $this->assertNotNull($segment->stats->averageSpeed); + $this->assertGreaterThan(0, $segment->stats->averageSpeed); + $this->assertNotNull($segment->stats->averagePace); + $this->assertGreaterThan(0, $segment->stats->averagePace); + + // Altitude bounds + $this->assertEqualsWithDelta(2418.88, $segment->stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(2425, $segment->stats->maxAltitude, 0.01); + + // Start/end coordinates + $this->assertEquals(46.571948, $segment->stats->startedAtCoords['lat']); + $this->assertEquals(46.572054, $segment->stats->finishedAtCoords['lat']); + } + + public function testSegmentStatsWithoutTimestamps(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, 100), + $this->makePoint(46.572016, 8.414866, 200), + ]; + $segment->recalculateStats(); + + // Distance should still be calculated + $this->assertGreaterThan(0, $segment->stats->distance); + + // Duration, speed, pace should be null (no timestamps) + $this->assertNull($segment->stats->duration); + $this->assertNull($segment->stats->averageSpeed); + $this->assertNull($segment->stats->averagePace); + } + + public function testSegmentStatsWithoutElevation(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, null, '2017-08-13T07:10:41Z'), + $this->makePoint(46.572016, 8.414866, null, '2017-08-13T07:10:54Z'), + ]; + $segment->recalculateStats(); + + $this->assertGreaterThan(0, $segment->stats->distance); + $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationGain, 0.001); + $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationLoss, 0.001); + } + + public function testSegmentStatsIgnoreElevationZero(): void + { + phpGPX::$IGNORE_ELEVATION_0 = true; + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, 100, '2017-08-13T07:10:41Z'), + $this->makePoint(46.572016, 8.414866, 0, '2017-08-13T07:10:54Z'), + $this->makePoint(46.572088, 8.414911, 200, '2017-08-13T07:11:56Z'), + ]; + $segment->recalculateStats(); + + // minAltitude should NOT be 0 when IGNORE_ELEVATION_0 is true + $this->assertGreaterThan(0, $segment->stats->minAltitude); + } + + public function testSegmentStatsRecalculateResetsValues(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, 100, '2017-08-13T07:10:41Z'), + $this->makePoint(46.572016, 8.414866, 200, '2017-08-13T07:10:54Z'), + ]; + $segment->recalculateStats(); + $firstDistance = $segment->stats->distance; + + // Recalculate again — should get same result (not accumulated) + $segment->recalculateStats(); + $this->assertEqualsWithDelta($firstDistance, $segment->stats->distance, 0.001); + } + + // --- Track stats --- + + public function testTrackStatsEmptySegments(): void + { + $track = new Track(); + $track->recalculateStats(); + + $this->assertInstanceOf(Stats::class, $track->stats); + $this->assertNull($track->stats->distance); + } + + public function testTrackStatsSingleSegment(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(46.571948, 8.414757, 2419, '2017-08-13T07:10:41Z'), + $this->makePoint(46.572016, 8.414866, 2425, '2017-08-13T07:10:54Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + $track->recalculateStats(); + + $this->assertGreaterThan(0, $track->stats->distance); + $this->assertEqualsWithDelta(6.0, $track->stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(2419.0, $track->stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(2425.0, $track->stats->maxAltitude, 0.01); + } + + public function testTrackStatsMultipleSegmentsAggregated(): void + { + $seg1 = new Segment(); + $seg1->points = [ + $this->makePoint(46.571948, 8.414757, 100, '2017-08-13T07:10:00Z'), + $this->makePoint(46.572016, 8.414866, 150, '2017-08-13T07:10:30Z'), + ]; + + $seg2 = new Segment(); + $seg2->points = [ + $this->makePoint(46.573000, 8.415000, 200, '2017-08-13T07:15:00Z'), + $this->makePoint(46.574000, 8.416000, 180, '2017-08-13T07:15:30Z'), + ]; + + $track = new Track(); + $track->segments = [$seg1, $seg2]; + $track->recalculateStats(); + + // Distances should be summed across segments + $seg1->recalculateStats(); + $seg2->recalculateStats(); + $expectedDistance = $seg1->stats->distance + $seg2->stats->distance; + $this->assertEqualsWithDelta($expectedDistance, $track->stats->distance, 0.01); + + // Elevation gain aggregated: seg1 has 50m gain, seg2 has 0 + $this->assertEqualsWithDelta(50.0, $track->stats->cumulativeElevationGain, 0.01); + + // Elevation loss aggregated: seg1 has 0, seg2 has 20m loss + $this->assertEqualsWithDelta(20.0, $track->stats->cumulativeElevationLoss, 0.01); + + // Min altitude should be minimum across all segments + $this->assertEqualsWithDelta(100.0, $track->stats->minAltitude, 0.01); + + // Max altitude should be maximum across all segments + $this->assertEqualsWithDelta(200.0, $track->stats->maxAltitude, 0.01); + + // Start/end should span the entire track + $this->assertNotNull($track->stats->startedAt); + $this->assertNotNull($track->stats->finishedAt); + + // Duration spans first point of first segment to last point of last segment + $this->assertEqualsWithDelta(330.0, $track->stats->duration, 0.1); + } + + public function testTrackGetPointsFlattensSegments(): void + { + $seg1 = new Segment(); + $seg1->points = [ + $this->makePoint(46.571948, 8.414757), + $this->makePoint(46.572016, 8.414866), + ]; + + $seg2 = new Segment(); + $seg2->points = [ + $this->makePoint(46.573000, 8.415000), + ]; + + $track = new Track(); + $track->segments = [$seg1, $seg2]; + + $allPoints = $track->getPoints(); + $this->assertCount(3, $allPoints); + } + + // --- Route stats --- + + public function testRouteStatsEmptyPoints(): void + { + $route = new Route(); + $route->recalculateStats(); + + $this->assertInstanceOf(Stats::class, $route->stats); + $this->assertNull($route->stats->distance); + } + + public function testRouteStatsBasic(): void + { + $route = new Route(); + $route->points = [ + $this->makeRoutePoint(54.9328621088893, 9.860624216140083, 0.0), + $this->makeRoutePoint(54.93293237320851, 9.86092208681491, 1.0), + $this->makeRoutePoint(54.93327743521187, 9.86187816543752, 2.0), + $this->makeRoutePoint(54.93342326167919, 9.862439849679859, 3.0), + ]; + $route->recalculateStats(); + + $this->assertGreaterThan(0, $route->stats->distance); + $this->assertEqualsWithDelta(3.0, $route->stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(0.0, $route->stats->cumulativeElevationLoss, 0.01); + $this->assertEqualsWithDelta(0.0, $route->stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(3.0, $route->stats->maxAltitude, 0.01); + } + + // --- Stats model --- + + public function testStatsReset(): void + { + $stats = new Stats(); + $stats->distance = 100.0; + $stats->duration = 60.0; + $stats->averageSpeed = 1.67; + $stats->cumulativeElevationGain = 50.0; + + $stats->reset(); + + $this->assertNull($stats->distance); + $this->assertNull($stats->duration); + $this->assertNull($stats->averageSpeed); + $this->assertNull($stats->cumulativeElevationGain); + $this->assertNull($stats->startedAt); + $this->assertNull($stats->finishedAt); + } + + public function testStatsToArray(): void + { + $stats = new Stats(); + $stats->distance = 1000.0; + $stats->realDistance = 1005.0; + $stats->averageSpeed = 2.5; + $stats->averagePace = 400.0; + $stats->minAltitude = 100.0; + $stats->maxAltitude = 200.0; + $stats->duration = 400.0; + + $array = $stats->toArray(); + + $this->assertEquals(1000.0, $array['distance']); + $this->assertEquals(1005.0, $array['realDistance']); + $this->assertEquals(2.5, $array['avgSpeed']); + $this->assertEquals(400.0, $array['avgPace']); + $this->assertEquals(100.0, $array['minAltitude']); + $this->assertEquals(200.0, $array['maxAltitude']); + $this->assertEquals(400.0, $array['duration']); + } + + public function testStatsJsonSerialize(): void + { + $stats = new Stats(); + $stats->distance = 500.0; + + $json = $stats->jsonSerialize(); + $this->assertEquals($stats->toArray(), $json); + } +} \ No newline at end of file diff --git a/tests/Unit/Parsers/BoundsParserTest.php b/tests/Unit/Parsers/BoundsParserTest.php new file mode 100644 index 0000000..4b3af32 --- /dev/null +++ b/tests/Unit/Parsers/BoundsParserTest.php @@ -0,0 +1,54 @@ +bounds = new Bounds( + 49.072489, + 18.814543, + 49.090543, + 18.886939 + ); + + $this->file = simplexml_load_file(self::FIXTURES_DIR . '/bounds.xml'); + } + + public function testParse(): void + { + $bounds = BoundsParser::parse($this->file->bounds); + + $this->assertEquals($bounds, $this->bounds); + $this->assertNotEmpty($bounds); + + $this->assertEquals($this->bounds->maxLatitude, $bounds->maxLatitude); + $this->assertEquals($this->bounds->maxLongitude, $bounds->maxLongitude); + $this->assertEquals($this->bounds->minLatitude, $bounds->minLatitude); + $this->assertEquals($this->bounds->minLongitude, $bounds->minLongitude); + + $this->assertEquals($this->bounds->jsonSerialize(), $bounds->jsonSerialize()); + } + + public function testToXML(): void + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(BoundsParser::toXML($this->bounds, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } +} \ No newline at end of file diff --git a/tests/Unit/Parsers/CopyrightParserTest.php b/tests/Unit/Parsers/CopyrightParserTest.php new file mode 100644 index 0000000..571056e --- /dev/null +++ b/tests/Unit/Parsers/CopyrightParserTest.php @@ -0,0 +1,58 @@ +copyright = new Copyright(); + $this->copyright->author = "Jakub Dubec"; + $this->copyright->license = "https://github.com/Sibyx/phpGPX/blob/master/LICENSE"; + $this->copyright->year = '2017'; + + $this->file = simplexml_load_file(self::FIXTURES_DIR . '/copyright.xml'); + } + + public function testParse(): void + { + $copyright = CopyrightParser::parse($this->file->copyright); + + $this->assertEquals($this->copyright, $copyright); + $this->assertNotEmpty($copyright); + + $this->assertEquals($this->copyright->author, $copyright->author); + $this->assertEquals($this->copyright->license, $copyright->license); + $this->assertEquals($this->copyright->year, $copyright->year); + + $this->assertEquals($this->copyright->toArray(), $copyright->toArray()); + } + + public function testToXML(): void + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(CopyrightParser::toXML($this->copyright, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + public function testToJSON(): void + { + $this->assertJsonStringEqualsJsonFile( + self::FIXTURES_DIR . '/copyright.json', json_encode($this->copyright->toArray()) + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Parsers/EmailParserTest.php b/tests/Unit/Parsers/EmailParserTest.php new file mode 100644 index 0000000..6d0f373 --- /dev/null +++ b/tests/Unit/Parsers/EmailParserTest.php @@ -0,0 +1,56 @@ +email = new Email(); + $this->email->id = "jakub.dubec"; + $this->email->domain = "gmail.com"; + + $this->file = simplexml_load_file(self::FIXTURES_DIR . '/email.xml'); + } + + public function testParse(): void + { + $email = EmailParser::parse($this->file->email); + + $this->assertEquals($this->email, $email); + $this->assertNotEmpty($email); + + $this->assertEquals($this->email->id, $email->id); + $this->assertEquals($this->email->domain, $email->domain); + + $this->assertEquals($this->email->toArray(), $email->toArray()); + } + + public function testToXML(): void + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(EmailParser::toXML($this->email, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + public function testToJSON(): void + { + $this->assertJsonStringEqualsJsonFile( + self::FIXTURES_DIR . '/email.json', json_encode($this->email->toArray()) + ); + } +} \ No newline at end of file diff --git a/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php b/tests/Unit/Parsers/ExtensionParserTest.php similarity index 59% rename from src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php rename to tests/Unit/Parsers/ExtensionParserTest.php index 95ebb9d..a459202 100644 --- a/src/phpGPX/Tests/Parsers/Extension/ExtensionParserTest.php +++ b/tests/Unit/Parsers/ExtensionParserTest.php @@ -1,6 +1,6 @@ aTemp = 14.0; - $trackpoint->hr = 152.0; + { + $trackpoint = new TrackPointExtension(); + $trackpoint->aTemp = 14.0; + $trackpoint->hr = 152.0; - $this->extensions = new Extensions(); - $this->extensions->trackPointExtension = $trackpoint; + $this->extensions = new Extensions(); + $this->extensions->trackPointExtension = $trackpoint; - $this->file = simplexml_load_file(sprintf("%s/extension.xml", __DIR__)); + $this->file = simplexml_load_file(self::FIXTURES_DIR . '/extension.xml'); } - /** - * @covers \phpGPX\Parsers\ExtensionParser - * @covers \phpGPX\Parsers\Extensions\TrackPointExtensionParser - * @covers \phpGPX\Models\Extensions - * @covers \phpGPX\Helpers\SerializationHelper - * @covers \phpGPX\Models\Extensions\AbstractExtension - * @covers \phpGPX\Models\Extensions\TrackPointExtension - * @return void - */ - public function testParse() + public function testParse(): void { $extensions = ExtensionParser::parse($this->file->extensions); - $this->assertEquals($this->extensions->unsupported, $extensions->unsupported); + $this->assertEquals($this->extensions->unsupported, $extensions->unsupported); $this->assertEquals( - $this->extensions->trackPointExtension->toArray(), $extensions->trackPointExtension->toArray() - ); + $this->extensions->trackPointExtension->toArray(), $extensions->trackPointExtension->toArray() + ); $this->assertEquals($this->extensions->toArray(), $extensions->toArray()); } - /** - * @covers \phpGPX\Parsers\ExtensionParser - * @covers \phpGPX\Parsers\Extensions\TrackPointExtensionParser - * @covers \phpGPX\Models\Extensions - * @covers \phpGPX\Models\Extensions\AbstractExtension - * @covers \phpGPX\Models\Extensions\TrackPointExtension - * @return void - * @throws \DOMException - */ - public function testToXML() + public function testToXML(): void { $document = new \DOMDocument("1.0", 'UTF-8'); @@ -80,17 +64,10 @@ public function testToXML() $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); } - /** - * @covers \phpGPX\Models\Extensions - * @covers \phpGPX\Models\Extensions\AbstractExtension - * @covers \phpGPX\Models\Extensions\TrackPointExtension - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testToJSON() - { - $this->assertJsonStringEqualsJsonFile( - sprintf("%s/extension.json", __DIR__), json_encode($this->extensions->toArray()) - ); - } -} + public function testToJSON(): void + { + $this->assertJsonStringEqualsJsonFile( + self::FIXTURES_DIR . '/extension.json', json_encode($this->extensions->toArray()) + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Parsers/LinkParserTest.php b/tests/Unit/Parsers/LinkParserTest.php new file mode 100644 index 0000000..fa9a2f4 --- /dev/null +++ b/tests/Unit/Parsers/LinkParserTest.php @@ -0,0 +1,63 @@ +link = new Link(); + $this->link->href = "https://jakubdubec.me"; + $this->link->text = "Portfolio"; + $this->link->type = "text/html"; + + $this->file = simplexml_load_file(self::FIXTURES_DIR . '/link.xml'); + } + + public function testParse(): void + { + $links = LinkParser::parse($this->file->link); + + $this->assertNotEmpty($links); + $this->assertCount(1, $links); + + $link = $links[0]; + $this->assertEquals($this->link->href, $link->href); + $this->assertEquals($this->link->text, $link->text); + $this->assertEquals($this->link->type, $link->type); + + $this->assertEquals($this->link->toArray(), $link->toArray()); + } + + public function testToXML(): void + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $xmlArray = LinkParser::toXMLArray([$this->link], $document); + + foreach ($xmlArray as $xmlElement) { + $root->appendChild($xmlElement); + } + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + public function testToJSON(): void + { + $this->assertJsonStringEqualsJsonFile( + self::FIXTURES_DIR . '/link.json', json_encode($this->link->toArray()) + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Parsers/PersonParserTest.php b/tests/Unit/Parsers/PersonParserTest.php new file mode 100644 index 0000000..d996bf6 --- /dev/null +++ b/tests/Unit/Parsers/PersonParserTest.php @@ -0,0 +1,92 @@ +person = new Person(); + $this->person->name = "Jakub Dubec"; + + $email = new Email(); + $email->id = "jakub.dubec"; + $email->domain = "gmail.com"; + $this->person->email = $email; + + $link = new Link(); + $link->href = "https://jakubdubec.me"; + $link->text = "Portfolio"; + $link->type = "text/html"; + $this->person->links[] = $link; + + $this->file = simplexml_load_file(self::FIXTURES_DIR . '/person.xml'); + } + + public function testParse(): void + { + $person = PersonParser::parse($this->file->author); + + $this->assertNotEmpty($person); + + $this->assertEquals($this->person->name, $person->name); + $this->assertEquals($this->person, $person); + + $this->assertEquals($this->person->email->id, $person->email->id); + $this->assertEquals($this->person->email->domain, $person->email->domain); + + $this->assertEquals($this->person->links[0]->type, $person->links[0]->type); + $this->assertEquals($this->person->links[0]->text, $person->links[0]->text); + $this->assertEquals($this->person->links[0]->href, $person->links[0]->href); + + $this->assertEquals($this->person->toArray(), $person->toArray()); + $this->assertEquals($this->person->email->toArray(), $person->email->toArray()); + $this->assertEquals($this->person->links[0]->toArray(), $person->links[0]->toArray()); + } + + /** + * @url https://github.com/Sibyx/phpGPX/issues/48 + */ + public function testEmptyLinks(): void + { + $gpx_file = new GpxFile(); + + $gpx_file->metadata = new Metadata(); + $gpx_file->metadata->author = new Person(); + $gpx_file->metadata->author->name = "Arthur Dent"; + + $this->assertNotNull($gpx_file->toXML()->saveXML()); + } + + public function testToXML(): void + { + $document = new \DOMDocument("1.0", 'UTF-8'); + + $root = $document->createElement("document"); + $root->appendChild(PersonParser::toXML($this->person, $document)); + + $document->appendChild($root); + + $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); + } + + public function testToJSON(): void + { + $this->assertJsonStringEqualsJsonFile( + self::FIXTURES_DIR . '/person.json', json_encode($this->person->toArray()) + ); + } +} \ No newline at end of file diff --git a/src/phpGPX/Tests/Parsers/Bounds/bounds.json b/tests/fixtures/Parsers/Bounds/bounds.json similarity index 100% rename from src/phpGPX/Tests/Parsers/Bounds/bounds.json rename to tests/fixtures/Parsers/Bounds/bounds.json diff --git a/src/phpGPX/Tests/Parsers/Bounds/bounds.xml b/tests/fixtures/Parsers/Bounds/bounds.xml similarity index 100% rename from src/phpGPX/Tests/Parsers/Bounds/bounds.xml rename to tests/fixtures/Parsers/Bounds/bounds.xml diff --git a/src/phpGPX/Tests/Parsers/Copyright/copyright.json b/tests/fixtures/Parsers/Copyright/copyright.json similarity index 100% rename from src/phpGPX/Tests/Parsers/Copyright/copyright.json rename to tests/fixtures/Parsers/Copyright/copyright.json diff --git a/src/phpGPX/Tests/Parsers/Copyright/copyright.xml b/tests/fixtures/Parsers/Copyright/copyright.xml similarity index 100% rename from src/phpGPX/Tests/Parsers/Copyright/copyright.xml rename to tests/fixtures/Parsers/Copyright/copyright.xml diff --git a/src/phpGPX/Tests/Parsers/Email/email.json b/tests/fixtures/Parsers/Email/email.json similarity index 100% rename from src/phpGPX/Tests/Parsers/Email/email.json rename to tests/fixtures/Parsers/Email/email.json diff --git a/src/phpGPX/Tests/Parsers/Email/email.xml b/tests/fixtures/Parsers/Email/email.xml similarity index 100% rename from src/phpGPX/Tests/Parsers/Email/email.xml rename to tests/fixtures/Parsers/Email/email.xml diff --git a/src/phpGPX/Tests/Parsers/Extension/extension.json b/tests/fixtures/Parsers/Extension/extension.json similarity index 100% rename from src/phpGPX/Tests/Parsers/Extension/extension.json rename to tests/fixtures/Parsers/Extension/extension.json diff --git a/src/phpGPX/Tests/Parsers/Extension/extension.xml b/tests/fixtures/Parsers/Extension/extension.xml similarity index 100% rename from src/phpGPX/Tests/Parsers/Extension/extension.xml rename to tests/fixtures/Parsers/Extension/extension.xml diff --git a/src/phpGPX/Tests/Parsers/Link/link.json b/tests/fixtures/Parsers/Link/link.json similarity index 100% rename from src/phpGPX/Tests/Parsers/Link/link.json rename to tests/fixtures/Parsers/Link/link.json diff --git a/src/phpGPX/Tests/Parsers/Link/link.xml b/tests/fixtures/Parsers/Link/link.xml similarity index 100% rename from src/phpGPX/Tests/Parsers/Link/link.xml rename to tests/fixtures/Parsers/Link/link.xml diff --git a/src/phpGPX/Tests/Parsers/Person/person.json b/tests/fixtures/Parsers/Person/person.json similarity index 100% rename from src/phpGPX/Tests/Parsers/Person/person.json rename to tests/fixtures/Parsers/Person/person.json diff --git a/src/phpGPX/Tests/Parsers/Person/person.xml b/tests/fixtures/Parsers/Person/person.xml similarity index 100% rename from src/phpGPX/Tests/Parsers/Person/person.xml rename to tests/fixtures/Parsers/Person/person.xml diff --git a/tests/phpGPX/Helpers/DateTimeHelperTest.php b/tests/phpGPX/Helpers/DateTimeHelperTest.php deleted file mode 100644 index 2771271..0000000 --- a/tests/phpGPX/Helpers/DateTimeHelperTest.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ - -namespace phpGPX\Helpers; - -use phpGPX\Models\Point; -use PHPUnit\Framework\TestCase; - -class DateTimeHelperTest extends TestCase -{ - /** - * @covers \phpGPX\Helpers\DateTimeHelper - * @covers \phpGPX\Models\Point - * @return void - * @throws \Exception - */ - public function testComparePointsByTimestamp() - { - // 2017-08-12T20:16:29+00:00 - $point1 = new Point(Point::WAYPOINT); - $time1 = new \DateTime("2017-08-12T20:16:29+00:00", new \DateTimeZone("UTC")); - $point1->time = $time1; - - // 2017-08-12T20:15:19+00:00 - $point2 = new Point(Point::WAYPOINT); - $time2 = new \DateTime("2017-08-12T20:15:19+00:00", new \DateTimeZone("UTC")); - $point2->time = $time2; - - $this->assertTrue(($time1 > $time2) && DateTimeHelper::comparePointsByTimestamp($point1, $point2)); - } - - /** - * @covers \phpGPX\Helpers\DateTimeHelper::formatDateTime - * @return void - */ - public function testFormatDateTime() - { - // 1. Basic test - $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); - - $this->assertEquals( - $datetime->format("Y-m-d H:i:s"), - DateTimeHelper::formatDateTime($datetime, "Y-m-d H:i:s") - ); - - // 2. NULL value - $datetime = null; - - $this->assertNull(DateTimeHelper::formatDateTime($datetime), "NULL input"); - - // 3. Empty string - $datetime = ""; - - $this->assertNull(DateTimeHelper::formatDateTime($datetime), "Empty string input"); - - // 4. Timezone - $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); - - $this->assertEquals( - "2017-08-12 21:16:29", - DateTimeHelper::formatDateTime($datetime, "Y-m-d H:i:s", '+01:00') - ); - } - - /** - * @covers \phpGPX\Helpers\DateTimeHelper::parseDateTime - * @return void - */ - public function testParseDateTime() - { - // 1. Valid string - $this->assertEquals( - new \DateTime("2017-08-12T20:16:29+00:00"), - DateTimeHelper::parseDateTime("2017-08-12T20:16:29+00:00") - ); - } - - /** - * @covers \phpGPX\Helpers\DateTimeHelper::parseDateTime - * @return void - */ - public function testParseDateTimeInvalidInput() - { - $this->expectException("Exception"); - DateTimeHelper::parseDateTime("Invalid exception"); - } -} diff --git a/tests/phpGPX/Helpers/SerializationHelperTest.php b/tests/phpGPX/Helpers/SerializationHelperTest.php deleted file mode 100644 index 4ee925d..0000000 --- a/tests/phpGPX/Helpers/SerializationHelperTest.php +++ /dev/null @@ -1,105 +0,0 @@ - - */ - -namespace phpGPX\Helpers; - -use PHPUnit\Framework\TestCase; - -class SerializationHelperTest extends TestCase -{ - /** - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testIntegerOrNull() - { - $this->assertNull(SerializationHelper::integerOrNull("")); - $this->assertNull(SerializationHelper::integerOrNull(null)); - $this->assertNull(SerializationHelper::integerOrNull("BLA")); - $this->assertIsInt(SerializationHelper::integerOrNull(5)); - $this->assertIsInt(SerializationHelper::integerOrNull("5")); - } - - /** - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testFloatOrNull() - { - $this->assertNull(SerializationHelper::floatOrNull("")); - $this->assertNull(SerializationHelper::floatOrNull(null)); - $this->assertNull(SerializationHelper::floatOrNull("BLA")); - $this->assertIsFloat(SerializationHelper::floatOrNull(5.6)); - $this->assertIsFloat(SerializationHelper::floatOrNull(5)); - $this->assertIsFloat(SerializationHelper::floatOrNull("5.6")); - $this->assertIsFloat(SerializationHelper::floatOrNull("5")); - } - - /** - * @covers \phpGPX\Helpers\SerializationHelper - * @return void - */ - public function testStringOrNull() - { - $this->assertNull(SerializationHelper::stringOrNull(null)); - $this->assertIsString(SerializationHelper::stringOrNull("")); - $this->assertIsString(SerializationHelper::stringOrNull("Bla bla")); - } - - /** - * @covers \phpGPX\Helpers\SerializationHelper - * @dataProvider dataProviderFilterNotNull - */ - public function testFilterNotNull($expected, $actual) - { - $this->assertEquals($expected, SerializationHelper::filterNotNull($actual)); - } - - public static function dataProviderFilterNotNull(): array -{ - return [ - 'numeric 1' => [ - [], - [null], - ], - 'numeric 2' => [ - [], - [null, [null]], - ], - 'numeric 3' => [ - [1 => 1], - [null, 1], - ], - 'numeric 4' => [ - [1 => 1, 3 => 2], - [null, 1, null, 2, null], - ], - 'numeric 5' => [ - [1 => 1, 3 => 2, 5 => [0 => 3, 2 => 4], 6 => 5], - [null, 1, null, 2, null, [3, null, 4], 5, null], - ], - 'associative 1' => [ - [], - ["foo" => null], - ], - 'associative 2' => [ - [], - ["foo" => null, ["bar" => null]], - ], - 'associative 3' => [ - ["bar" => 1], - ["foo" => null, "bar" => 1], - ], - 'associative 4' => [ - ["bar" => 1, "caw" => 2], - ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null], - ], - 'associative 5' => [ - ["bar" => 1, "caw" => 2, "ere" => ["foo" => 3, "baz" => 4], "moo" => 5], - ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null, "ere" => ["foo" => 3, "bar" => null, "baz" => 4], "moo" => 5, "boo" => null], - ], - ]; - } -} diff --git a/tests/phpGPX/Models/BoundsTest.php b/tests/phpGPX/Models/BoundsTest.php deleted file mode 100644 index b51437e..0000000 --- a/tests/phpGPX/Models/BoundsTest.php +++ /dev/null @@ -1,22 +0,0 @@ -bounds = new Bounds( - 49.072489, - 18.814543, - 49.090543, - 18.886939 - ); - } -} \ No newline at end of file From ed6f2534dc8ad41ef99e6ae5761dad45ab8ff5d3 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Fri, 6 Mar 2026 03:18:13 +0100 Subject: [PATCH 09/31] Consolidate workflows, add documentation, and update project infrastructure. --- .github/workflows/ci.yml | 80 ++++++++++++++ .github/workflows/coverage.yml | 24 ---- .github/workflows/phpunit.yml | 21 ---- .gitignore | 6 +- Dockerfile | 22 ++++ composer.json | 5 + docker-compose.yml | 7 ++ docs/00_Getting_Started/01_Installation.md | 18 +++ docs/00_Getting_Started/02_Quick_Start.md | 64 +++++++++++ docs/00_Getting_Started/_index.md | 3 + docs/01_Usage/01_Loading_Files.md | 54 +++++++++ docs/01_Usage/02_Creating_Files.md | 123 +++++++++++++++++++++ docs/01_Usage/03_Statistics.md | 83 ++++++++++++++ docs/01_Usage/04_Configuration.md | 42 +++++++ docs/01_Usage/05_Extensions.md | 68 ++++++++++++ docs/01_Usage/_index.md | 3 + docs/02_Output_Formats/01_XML.md | 45 ++++++++ docs/02_Output_Formats/02_JSON.md | 68 ++++++++++++ docs/02_Output_Formats/03_GeoJSON.md | 73 ++++++++++++ docs/02_Output_Formats/_index.md | 3 + docs/03_API_Reference/_index.md | 11 ++ docs/04_Development/01_Contributing.md | 30 +++++ docs/04_Development/02_Testing.md | 41 +++++++ docs/04_Development/_index.md | 3 + docs/config.json | 6 + docs/index.md | 33 ++++++ phpdoc.xml | 32 ++++++ 27 files changed, 920 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/coverage.yml delete mode 100644 .github/workflows/phpunit.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docs/00_Getting_Started/01_Installation.md create mode 100644 docs/00_Getting_Started/02_Quick_Start.md create mode 100644 docs/00_Getting_Started/_index.md create mode 100644 docs/01_Usage/01_Loading_Files.md create mode 100644 docs/01_Usage/02_Creating_Files.md create mode 100644 docs/01_Usage/03_Statistics.md create mode 100644 docs/01_Usage/04_Configuration.md create mode 100644 docs/01_Usage/05_Extensions.md create mode 100644 docs/01_Usage/_index.md create mode 100644 docs/02_Output_Formats/01_XML.md create mode 100644 docs/02_Output_Formats/02_JSON.md create mode 100644 docs/02_Output_Formats/03_GeoJSON.md create mode 100644 docs/02_Output_Formats/_index.md create mode 100644 docs/03_API_Reference/_index.md create mode 100644 docs/04_Development/01_Contributing.md create mode 100644 docs/04_Development/02_Testing.md create mode 100644 docs/04_Development/_index.md create mode 100644 docs/config.json create mode 100644 docs/index.md create mode 100644 phpdoc.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1f2aaf0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: ['8.1', '8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: dom, simplexml, libxml + coverage: none + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run tests + run: vendor/bin/phpunit --configuration phpunit.xml + + coverage: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: dom, simplexml, libxml + coverage: xdebug + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run tests with coverage + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --coverage-clover coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + docs: + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: dom, simplexml, libxml + + - name: Install phpDocumentor + run: curl -sL https://phpdoc.org/phpDocumentor.phar -o /usr/local/bin/phpdoc && chmod +x /usr/local/bin/phpdoc + + - name: Build docs + run: phpdoc run --config phpdoc.xml + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: docs-output/ \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 915acf6..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Coverage - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: php-actions/composer@v6 - name: Install dependencies - - name: PHPUnit Tests - uses: php-actions/phpunit@v3 - env: - XDEBUG_MODE: coverage - with: - bootstrap: vendor/autoload.php - configuration: phpunit.xml - php_extensions: xdebug - args: --coverage-clover ./coverage.xml - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml deleted file mode 100644 index ba4f9a0..0000000 --- a/.github/workflows/phpunit.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: PHPUnit - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - php-version: ['8.1', '8.2'] - - steps: - - uses: actions/checkout@v3 - - uses: php-actions/composer@v6 - name: Install dependencies - with: - php_version: ${{ matrix.php-version }} - version: 2 - - name: Running PHPUnit - run: php vendor/bin/phpunit --configuration phpunit.xml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 40a1a33..7d9decf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,11 @@ composer.phar composer.lock /vendor/ /.idea/ -/phpdocs/ -/bin/ .DS_Store? *.DS_Store /.php_cs.cache /.phpunit.result.cache /.phpunit.cache -coverage.xml \ No newline at end of file +coverage.xml +/docs-output/ +/.phpdoc/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b7ddd8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM php:8.4-cli + +RUN apt-get update && apt-get install -y \ + libxml2-dev \ + git \ + unzip \ + curl \ + && docker-php-ext-install dom simplexml \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN echo "xdebug.mode=debug,coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.start_with_request=trigger" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN curl -sL https://phpdoc.org/phpDocumentor.phar -o /usr/local/bin/phpdoc \ + && chmod +x /usr/local/bin/phpdoc + +WORKDIR /app \ No newline at end of file diff --git a/composer.json b/composer.json index eb749ac..079ad61 100644 --- a/composer.json +++ b/composer.json @@ -35,5 +35,10 @@ }, "autoload-dev": { "psr-4": { "phpGPX\\Tests\\": "tests" } + }, + "scripts": { + "test": "phpunit", + "test:unit": "phpunit --testsuite unit", + "test:integration": "phpunit --testsuite integration" } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..621a434 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + php: + build: . + volumes: + - .:/app + environment: + - XDEBUG_MODE=debug,coverage \ No newline at end of file diff --git a/docs/00_Getting_Started/01_Installation.md b/docs/00_Getting_Started/01_Installation.md new file mode 100644 index 0000000..f89cb92 --- /dev/null +++ b/docs/00_Getting_Started/01_Installation.md @@ -0,0 +1,18 @@ +# Installation + +## Composer + +Install phpGPX via [Composer](https://getcomposer.org/): + +```bash +composer require sibyx/phpgpx +``` + +## Requirements + +- PHP >= 8.1 +- `ext-simplexml` - for XML parsing +- `ext-dom` - for XML generation +- `lib-libxml` - XML support library + +These extensions are included in most PHP installations by default. \ No newline at end of file diff --git a/docs/00_Getting_Started/02_Quick_Start.md b/docs/00_Getting_Started/02_Quick_Start.md new file mode 100644 index 0000000..74cfdef --- /dev/null +++ b/docs/00_Getting_Started/02_Quick_Start.md @@ -0,0 +1,64 @@ +# Quick Start + +## Loading a GPX file + +```php +use phpGPX\phpGPX; + +$file = phpGPX::load('path/to/file.gpx'); +``` + +You can also parse GPX data from a string: + +```php +$xml = file_get_contents('path/to/file.gpx'); +$file = phpGPX::parse($xml); +``` + +## Accessing data + +A `GpxFile` contains three main collections: + +```php +// Waypoints - individual points of interest +foreach ($file->waypoints as $waypoint) { + echo sprintf("%s: %f, %f\n", $waypoint->name, $waypoint->latitude, $waypoint->longitude); +} + +// Tracks - ordered lists of points recorded by a GPS device +foreach ($file->tracks as $track) { + echo $track->name . "\n"; + echo "Distance: " . $track->stats->distance . " m\n"; + + foreach ($track->segments as $segment) { + foreach ($segment->points as $point) { + echo sprintf(" %f, %f @ %s\n", $point->latitude, $point->longitude, $point->time->format('c')); + } + } +} + +// Routes - ordered lists of waypoints representing a planned path +foreach ($file->routes as $route) { + echo $route->name . "\n"; + foreach ($route->points as $point) { + echo sprintf(" %f, %f\n", $point->latitude, $point->longitude); + } +} +``` + +## Saving to file + +```php +use phpGPX\phpGPX; + +$file = phpGPX::load('input.gpx'); + +// Save as GPX XML +$file->save('output.gpx', phpGPX::XML_FORMAT); + +// Save as JSON +$file->save('output.json', phpGPX::JSON_FORMAT); + +// Save as GeoJSON +$file->save('output.geojson', phpGPX::GEOJSON_FORMAT); +``` \ No newline at end of file diff --git a/docs/00_Getting_Started/_index.md b/docs/00_Getting_Started/_index.md new file mode 100644 index 0000000..9dcecf8 --- /dev/null +++ b/docs/00_Getting_Started/_index.md @@ -0,0 +1,3 @@ +# Getting Started + +This section covers installation and first steps with phpGPX. \ No newline at end of file diff --git a/docs/01_Usage/01_Loading_Files.md b/docs/01_Usage/01_Loading_Files.md new file mode 100644 index 0000000..c2d4f2b --- /dev/null +++ b/docs/01_Usage/01_Loading_Files.md @@ -0,0 +1,54 @@ +# Loading Files + +## From file path + +The simplest way to load a GPX file: + +```php +use phpGPX\phpGPX; + +$file = phpGPX::load('/path/to/track.gpx'); +``` + +## From string + +Parse GPX XML directly from a string, useful when receiving data from an API or database: + +```php +$xml = ' + My Track + 2419 + +'; + +$file = phpGPX::parse($xml); +``` + +## What gets parsed + +When loading a GPX file, phpGPX processes: + +- **Metadata** - file name, description, author, copyright, time, bounds +- **Waypoints** (``) - individual points with coordinates, elevation, time, and all optional GPX 1.1 attributes +- **Tracks** (``) - containing segments (``) of track points (``) +- **Routes** (``) - containing route points (``) +- **Extensions** - Garmin TrackPointExtension (heart rate, temperature, cadence) and unsupported extensions preserved as key-value pairs + +## Automatic statistics + +By default, statistics are calculated automatically when loading a file. This includes distance, elevation gain/loss, duration, speed, and pace for each track, segment, and route. + +To disable automatic stats calculation: + +```php +phpGPX::$CALCULATE_STATS = false; + +$file = phpGPX::load('track.gpx'); +// $file->tracks[0]->stats will be null +``` + +You can recalculate stats manually at any time: + +```php +$file->tracks[0]->recalculateStats(); +``` \ No newline at end of file diff --git a/docs/01_Usage/02_Creating_Files.md b/docs/01_Usage/02_Creating_Files.md new file mode 100644 index 0000000..d87b045 --- /dev/null +++ b/docs/01_Usage/02_Creating_Files.md @@ -0,0 +1,123 @@ +# Creating Files + +You can build GPX files programmatically. + +## Building a track from scratch + +```php +use phpGPX\Models\GpxFile; +use phpGPX\Models\Metadata; +use phpGPX\Models\Point; +use phpGPX\Models\Segment; +use phpGPX\Models\Track; +use phpGPX\phpGPX; + +$gpxFile = new GpxFile(); + +// Optional metadata +$gpxFile->metadata = new Metadata(); +$gpxFile->metadata->time = new \DateTime(); +$gpxFile->metadata->description = "Morning run"; + +// Create a track +$track = new Track(); +$track->name = "Run 2024-01-15"; +$track->type = "running"; + +// Create a segment with points +$segment = new Segment(); + +$points = [ + ['lat' => 48.157, 'lon' => 17.054, 'ele' => 134, 'time' => '2024-01-15T07:00:00Z'], + ['lat' => 48.158, 'lon' => 17.055, 'ele' => 136, 'time' => '2024-01-15T07:00:30Z'], + ['lat' => 48.160, 'lon' => 17.057, 'ele' => 140, 'time' => '2024-01-15T07:01:00Z'], +]; + +foreach ($points as $data) { + $point = new Point(Point::TRACKPOINT); + $point->latitude = $data['lat']; + $point->longitude = $data['lon']; + $point->elevation = $data['ele']; + $point->time = new \DateTime($data['time']); + $segment->points[] = $point; +} + +$track->segments[] = $segment; + +// Calculate statistics +$track->recalculateStats(); + +$gpxFile->tracks[] = $track; + +// Save +$gpxFile->save('morning_run.gpx', phpGPX::XML_FORMAT); +``` + +## Building a route + +```php +use phpGPX\Models\GpxFile; +use phpGPX\Models\Point; +use phpGPX\Models\Route; + +$gpxFile = new GpxFile(); + +$route = new Route(); +$route->name = "Hiking trail"; + +$waypoints = [ + ['lat' => 46.571, 'lon' => 8.414, 'ele' => 2419, 'name' => 'Start'], + ['lat' => 46.580, 'lon' => 8.420, 'ele' => 2600, 'name' => 'Summit'], + ['lat' => 46.575, 'lon' => 8.418, 'ele' => 2450, 'name' => 'Hut'], +]; + +foreach ($waypoints as $data) { + $point = new Point(Point::ROUTEPOINT); + $point->latitude = $data['lat']; + $point->longitude = $data['lon']; + $point->elevation = $data['ele']; + $point->name = $data['name']; + $route->points[] = $point; +} + +$route->recalculateStats(); + +$gpxFile->routes[] = $route; +$gpxFile->save('trail.gpx', \phpGPX\phpGPX::XML_FORMAT); +``` + +## Adding waypoints + +```php +use phpGPX\Models\GpxFile; +use phpGPX\Models\Link; +use phpGPX\Models\Point; + +$gpxFile = new GpxFile(); + +$waypoint = new Point(Point::WAYPOINT); +$waypoint->latitude = 48.8566; +$waypoint->longitude = 2.3522; +$waypoint->elevation = 35; +$waypoint->name = "Eiffel Tower"; +$waypoint->description = "Famous landmark in Paris"; +$waypoint->symbol = "Landmark"; + +$link = new Link(); +$link->href = "https://www.toureiffel.paris"; +$link->text = "Official website"; +$waypoint->links[] = $link; + +$gpxFile->waypoints[] = $waypoint; +$gpxFile->save('places.gpx', \phpGPX\phpGPX::XML_FORMAT); +``` + +## Direct XML output to browser + +```php +header("Content-Type: application/gpx+xml"); +header("Content-Disposition: attachment; filename=track.gpx"); + +echo $gpxFile->toXML()->saveXML(); +exit(); +``` \ No newline at end of file diff --git a/docs/01_Usage/03_Statistics.md b/docs/01_Usage/03_Statistics.md new file mode 100644 index 0000000..b9d2d98 --- /dev/null +++ b/docs/01_Usage/03_Statistics.md @@ -0,0 +1,83 @@ +# Statistics + +phpGPX automatically calculates statistics for tracks, segments, and routes when loading GPX files. + +## Available statistics + +The `Stats` object provides: + +| Property | Type | Description | +|----------|------|-------------| +| `distance` | float | Distance in meters (2D, horizontal only) | +| `realDistance` | float | Distance in meters including elevation changes (3D) | +| `averageSpeed` | float | Average speed in m/s | +| `averagePace` | float | Average pace in s/km | +| `minAltitude` | float | Minimum elevation in meters | +| `maxAltitude` | float | Maximum elevation in meters | +| `cumulativeElevationGain` | float | Total ascent in meters | +| `cumulativeElevationLoss` | float | Total descent in meters | +| `startedAt` | DateTime | Timestamp of first point | +| `finishedAt` | DateTime | Timestamp of last point | +| `duration` | float | Total duration in seconds | + +Coordinate properties are also available: `startedAtCoords`, `finishedAtCoords`, `minAltitudeCoords`, `maxAltitudeCoords` — each an array with `lat` and `lng` keys. + +## Accessing statistics + +```php +$file = phpGPX::load('track.gpx'); + +foreach ($file->tracks as $track) { + $stats = $track->stats; + + echo "Distance: " . round($stats->distance) . " m\n"; + echo "Real distance: " . round($stats->realDistance) . " m\n"; + echo "Elevation gain: " . round($stats->cumulativeElevationGain) . " m\n"; + echo "Duration: " . gmdate("H:i:s", $stats->duration) . "\n"; + echo "Average speed: " . round($stats->averageSpeed * 3.6, 1) . " km/h\n"; + + // Per-segment stats + foreach ($track->segments as $i => $segment) { + echo " Segment $i: " . round($segment->stats->distance) . " m\n"; + } +} +``` + +## Recalculating statistics + +After modifying points, recalculate: + +```php +$track->recalculateStats(); +``` + +For tracks, this recalculates each segment's stats first, then aggregates them. + +## Distance smoothing + +GPS noise can inflate distance measurements. Enable smoothing to filter out small movements: + +```php +phpGPX::$APPLY_DISTANCE_SMOOTHING = true; +phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; // meters — ignore movements smaller than this +``` + +## Elevation smoothing + +GPS altitude data is often noisy. Smoothing helps get more accurate elevation gain/loss: + +```php +phpGPX::$APPLY_ELEVATION_SMOOTHING = true; +phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; // meters — minimum change to count + +// Optional: filter spikes (e.g. GPS glitches showing 100m jumps) +phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = 50; // meters — maximum change to count +``` + +## Ignoring zero elevation + +Some GPS devices record elevation as 0 when they lose satellite fix. Ignore these points: + +```php +phpGPX::$IGNORE_ELEVATION_0 = true; +``` \ No newline at end of file diff --git a/docs/01_Usage/04_Configuration.md b/docs/01_Usage/04_Configuration.md new file mode 100644 index 0000000..8e777db --- /dev/null +++ b/docs/01_Usage/04_Configuration.md @@ -0,0 +1,42 @@ +# Configuration + +phpGPX is configured through static properties on the `phpGPX` class. Set these before loading or creating files. + +## All options + +```php +use phpGPX\phpGPX; + +// Calculate statistics automatically on load (default: true) +phpGPX::$CALCULATE_STATS = true; + +// Sort points by timestamp when loading (default: false) +phpGPX::$SORT_BY_TIMESTAMP = false; + +// DateTime format for JSON output (default: 'c' — ISO 8601) +phpGPX::$DATETIME_FORMAT = 'c'; + +// Timezone for DateTime output (default: null — uses UTC) +phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'UTC'; + +// Pretty print XML and JSON output (default: true) +phpGPX::$PRETTY_PRINT = true; + +// Ignore elevation values of 0 in stats (default: false) +phpGPX::$IGNORE_ELEVATION_0 = false; + +// Distance smoothing (default: false) +phpGPX::$APPLY_DISTANCE_SMOOTHING = false; +phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; // meters + +// Elevation smoothing (default: false) +phpGPX::$APPLY_ELEVATION_SMOOTHING = false; +phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; // meters +phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; // meters, or null to disable +``` + +## Notes + +- All configuration is global via static properties. There is no per-file configuration. +- Settings affect both loading (parsing + stats calculation) and saving (serialization format). +- The `$SORT_BY_TIMESTAMP` option is useful for GPX files where points are out of order, but is disabled by default since most files are already sorted. \ No newline at end of file diff --git a/docs/01_Usage/05_Extensions.md b/docs/01_Usage/05_Extensions.md new file mode 100644 index 0000000..4d23a78 --- /dev/null +++ b/docs/01_Usage/05_Extensions.md @@ -0,0 +1,68 @@ +# Extensions + +GPX 1.1 supports vendor-specific extensions. phpGPX parses known extensions into typed objects and preserves unknown ones. + +## Garmin TrackPointExtension + +The most common extension. Provides sensor data per track point. + +### Available fields + +| Property | Type | Description | +|----------|------|-------------| +| `aTemp` | float | Air temperature in degrees Celsius | +| `wTemp` | float | Water temperature in degrees Celsius | +| `depth` | float | Depth in meters | +| `hr` | float | Heart rate in beats per minute | +| `cad` | float | Cadence in revolutions per minute | +| `speed` | float | Speed in meters per second | +| `course` | int | Course in degrees from true north | +| `bearing` | int | Bearing in degrees from true north | + +### Reading extensions + +```php +$file = phpGPX::load('garmin_track.gpx'); + +foreach ($file->tracks as $track) { + foreach ($track->segments as $segment) { + foreach ($segment->points as $point) { + if ($point->extensions && $point->extensions->trackPointExtension) { + $ext = $point->extensions->trackPointExtension; + echo "HR: " . $ext->hr . " bpm\n"; + echo "Temp: " . $ext->aTemp . " C\n"; + } + } + } +} +``` + +### Writing extensions + +```php +use phpGPX\Models\Extensions; +use phpGPX\Models\Extensions\TrackPointExtension; + +$ext = new TrackPointExtension(); +$ext->hr = 145.0; +$ext->aTemp = 22.0; + +$extensions = new Extensions(); +$extensions->trackPointExtension = $ext; + +$point->extensions = $extensions; +``` + +The correct XML namespaces are handled automatically during serialization. + +## Unsupported extensions + +Extensions that phpGPX does not have a dedicated parser for are preserved as key-value pairs: + +```php +// Access unsupported extensions +$unsupported = $point->extensions->unsupported; +// e.g. ['MxTimeZeroSymbol' => 10, 'color' => -16744448] +``` + +Unsupported extensions are preserved during round-trip (load + save) and accessible through the `unsupported` array on the `Extensions` object. \ No newline at end of file diff --git a/docs/01_Usage/_index.md b/docs/01_Usage/_index.md new file mode 100644 index 0000000..fa186b1 --- /dev/null +++ b/docs/01_Usage/_index.md @@ -0,0 +1,3 @@ +# Usage + +Detailed guides on phpGPX features. \ No newline at end of file diff --git a/docs/02_Output_Formats/01_XML.md b/docs/02_Output_Formats/01_XML.md new file mode 100644 index 0000000..5903615 --- /dev/null +++ b/docs/02_Output_Formats/01_XML.md @@ -0,0 +1,45 @@ +# XML (GPX) + +The native GPX format, conforming to the [GPX 1.1 specification](http://www.topografix.com/GPX/1/1/). + +## Saving to file + +```php +$file->save('output.gpx', phpGPX::XML_FORMAT); +``` + +## Getting XML as string + +```php +$document = $file->toXML(); // Returns \DOMDocument +$xmlString = $document->saveXML(); +``` + +## Pretty printing + +By default, XML output is formatted with indentation: + +```php +phpGPX::$PRETTY_PRINT = true; // default +``` + +Set to `false` for compact output. + +## Namespaces + +Extension namespaces are included automatically when the file contains extensions. For example, a file with Garmin TrackPointExtension will include: + +```xml + +``` + +## Creator attribute + +The `creator` attribute is set from `$gpxFile->creator`. If not set, it defaults to the phpGPX library signature: + +```php +$gpxFile->creator = "My Application"; +``` \ No newline at end of file diff --git a/docs/02_Output_Formats/02_JSON.md b/docs/02_Output_Formats/02_JSON.md new file mode 100644 index 0000000..23b5c97 --- /dev/null +++ b/docs/02_Output_Formats/02_JSON.md @@ -0,0 +1,68 @@ +# JSON + +A structured JSON representation that mirrors the GPX data model. + +## Saving to file + +```php +$file->save('output.json', phpGPX::JSON_FORMAT); +``` + +## Getting JSON as string + +```php +$jsonString = $file->toJSON(false); // false = GPX structure format +``` + +## Structure + +The JSON output follows the GPX structure: + +```json +{ + "creator": "phpGPX/2.0.0-alpha.1", + "metadata": { + "name": "My Track", + "time": "2024-01-15T07:00:00+00:00" + }, + "tracks": [ + { + "name": "Morning Run", + "trkseg": [ + { + "points": [ + { + "lat": 48.157, + "lon": 17.054, + "ele": 134.0, + "time": "2024-01-15T07:00:00+00:00" + } + ], + "stats": { + "distance": 1250.5, + "avgSpeed": 3.2 + } + } + ], + "stats": { } + } + ] +} +``` + +## DateTime format + +Control the DateTime output format: + +```php +phpGPX::$DATETIME_FORMAT = 'c'; // ISO 8601 (default) +phpGPX::$DATETIME_FORMAT = 'U'; // Unix timestamp +phpGPX::$DATETIME_FORMAT = 'Y-m-d H:i:s'; // Custom +``` + +## Timezone + +```php +phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'UTC'; // default +phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'Europe/Prague'; // local time +``` \ No newline at end of file diff --git a/docs/02_Output_Formats/03_GeoJSON.md b/docs/02_Output_Formats/03_GeoJSON.md new file mode 100644 index 0000000..a3a6238 --- /dev/null +++ b/docs/02_Output_Formats/03_GeoJSON.md @@ -0,0 +1,73 @@ +# GeoJSON + +phpGPX can output data in [GeoJSON](https://geojson.org/) format (RFC 7946), which is widely supported by mapping libraries like Leaflet, Mapbox, and OpenLayers. + +## Saving to file + +```php +$file->save('output.geojson', phpGPX::GEOJSON_FORMAT); +``` + +## Getting GeoJSON as string + +```php +$geoJsonString = $file->toJSON(true); // true = GeoJSON format +``` + +## Structure + +The output is a GeoJSON `FeatureCollection`: + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [17.054, 48.157, 134.0] + }, + "properties": { + "name": "Waypoint 1", + "ele": 134.0, + "time": "2024-01-15T07:00:00+00:00" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiLineString", + "coordinates": [ + [[17.054, 48.157, 134.0], [17.055, 48.158, 136.0]] + ] + }, + "properties": { + "name": "Morning Run", + "stats": { "distance": 1250.5 } + } + } + ], + "metadata": { } +} +``` + +## Geometry type mapping + +| GPX element | GeoJSON geometry | +|-------------|-----------------| +| Waypoint (``) | `Point` | +| Route (``) | `LineString` | +| Track (``) | `MultiLineString` (one line per segment) | + +## Coordinate order + +GeoJSON uses `[longitude, latitude, elevation]` order, which is different from the GPX `lat/lon` order. phpGPX handles this conversion automatically. + +## Using with Leaflet + +```javascript +fetch('output.geojson') + .then(r => r.json()) + .then(data => L.geoJSON(data).addTo(map)); +``` \ No newline at end of file diff --git a/docs/02_Output_Formats/_index.md b/docs/02_Output_Formats/_index.md new file mode 100644 index 0000000..9f6dd19 --- /dev/null +++ b/docs/02_Output_Formats/_index.md @@ -0,0 +1,3 @@ +# Output Formats + +phpGPX supports three output formats: XML (GPX), JSON, and GeoJSON. \ No newline at end of file diff --git a/docs/03_API_Reference/_index.md b/docs/03_API_Reference/_index.md new file mode 100644 index 0000000..fa5cb2c --- /dev/null +++ b/docs/03_API_Reference/_index.md @@ -0,0 +1,11 @@ +# API Reference + +The API reference is auto-generated from PHPDoc comments by [phpDocumentor](https://www.phpdoc.org/). + +Build the full documentation site (API reference + guides) with: + +```bash +composer run docs:build +``` + +The output goes to `docs-output/`. API class pages are under `docs-output/classes/`. \ No newline at end of file diff --git a/docs/04_Development/01_Contributing.md b/docs/04_Development/01_Contributing.md new file mode 100644 index 0000000..7cd5d3d --- /dev/null +++ b/docs/04_Development/01_Contributing.md @@ -0,0 +1,30 @@ +# Contributing + +## Repository structure + +- `src/phpGPX/` - Library source code + - `Models/` - Data models (GpxFile, Track, Segment, Point, Stats, etc.) + - `Parsers/` - XML parsing and serialization + - `Helpers/` - Utility classes (GeoHelper, DateTimeHelper, distance/elevation calculators) +- `tests/` - Test suite + - `Unit/` - Unit tests for individual components + - `Integration/` - Full file load/save round-trip tests + - `fixtures/` - GPX test fixture files +- `docs/` - Documentation (Daux.io) + +## Branches + +- `master` - Latest stable release +- `develop` - Work on the next major version (2.x) + +## Setting up + +```bash +git clone https://github.com/Sibyx/phpGPX.git +cd phpGPX +composer install +``` + +## Code style + +The project uses PSR-2 with **tab indentation** (configured in `.php-cs-fixer.php`). \ No newline at end of file diff --git a/docs/04_Development/02_Testing.md b/docs/04_Development/02_Testing.md new file mode 100644 index 0000000..aa17c41 --- /dev/null +++ b/docs/04_Development/02_Testing.md @@ -0,0 +1,41 @@ +# Testing + +## Running tests + +```bash +# All tests +php vendor/bin/phpunit + +# Unit tests only +php vendor/bin/phpunit --testsuite unit + +# Integration tests only +php vendor/bin/phpunit --testsuite integration + +# Single test file +php vendor/bin/phpunit tests/Unit/Models/StatsCalculationTest.php + +# Single test method +php vendor/bin/phpunit --filter testSegmentStatsBasicTrack +``` + +## Test structure + +| Directory | Purpose | +|-----------|---------| +| `tests/Unit/Helpers/` | Helper classes: GeoHelper, DateTimeHelper, SerializationHelper, DistanceCalculator, ElevationGainLossCalculator | +| `tests/Unit/Models/` | Model logic: Bounds, Stats calculation (Segment, Track, Route) | +| `tests/Unit/Parsers/` | Parser round-trip: parse XML, verify data, serialize back to XML, compare | +| `tests/Integration/` | Full pipeline: load GPX files, test serialization, XML round-trips, GeoJSON output | + +## Fixture files + +Test fixtures are in `tests/fixtures/` (GPX files) and `tests/Fixtures/Parsers/` (XML/JSON fragments for parser tests). + +## Writing parser tests + +Parser tests follow a consistent pattern. Each parser has fixtures (XML input, expected JSON output) and tests three operations: + +1. **Parse** - load XML fixture, verify model properties +2. **toXML** - serialize model to XML, compare with original fixture +3. **toJSON** - serialize model to JSON, compare with expected JSON fixture \ No newline at end of file diff --git a/docs/04_Development/_index.md b/docs/04_Development/_index.md new file mode 100644 index 0000000..22b8154 --- /dev/null +++ b/docs/04_Development/_index.md @@ -0,0 +1,3 @@ +# Development + +Information for contributors and developers. \ No newline at end of file diff --git a/docs/config.json b/docs/config.json new file mode 100644 index 0000000..eb1bba7 --- /dev/null +++ b/docs/config.json @@ -0,0 +1,6 @@ +{ + "title": "phpGPX", + "tagline": "A PHP library for reading and creating GPX files", + "author": "Jakub Dubec", + "repo": "https://github.com/Sibyx/phpGPX" +} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..68a3216 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,33 @@ +--- +title: phpGPX Documentation +--- + +# phpGPX + +A PHP library for reading, creating, and manipulating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format). + +## Features + +- Full support of [GPX 1.1 specification](http://www.topografix.com/GPX/1/1/) +- Statistics calculation (distance, elevation, speed, pace, duration) +- Extension support (Garmin TrackPointExtension) +- Output in XML, JSON, and GeoJSON formats + +## Quick Example + +```php +use phpGPX\phpGPX; + +$file = phpGPX::load('track.gpx'); + +foreach ($file->tracks as $track) { + echo $track->stats->distance . " meters\n"; + echo $track->stats->cumulativeElevationGain . " meters gained\n"; +} +``` + +## Requirements + +- PHP >= 8.1 +- `ext-simplexml` +- `ext-dom` \ No newline at end of file diff --git a/phpdoc.xml b/phpdoc.xml new file mode 100644 index 0000000..5b750e1 --- /dev/null +++ b/phpdoc.xml @@ -0,0 +1,32 @@ + + + phpGPX API Documentation + + + docs-output + + + + + + src/phpGPX + + api + + phpGPX + + + + docs + + guide + + + \ No newline at end of file From fcac23befac3c532211e0ec2f5296e07dc7fba2d Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Fri, 6 Mar 2026 03:30:23 +0100 Subject: [PATCH 10/31] Updated PHPUnit requirements and configuration: allowed broader PHPUnit versions, updated schema to 12.5, and adjusted deprecation handling --- composer.json | 2 +- phpunit.xml | 6 +++--- tests/{fixtures => Fixtures}/Parsers/Bounds/bounds.json | 0 tests/{fixtures => Fixtures}/Parsers/Bounds/bounds.xml | 0 .../{fixtures => Fixtures}/Parsers/Copyright/copyright.json | 0 .../{fixtures => Fixtures}/Parsers/Copyright/copyright.xml | 0 tests/{fixtures => Fixtures}/Parsers/Email/email.json | 0 tests/{fixtures => Fixtures}/Parsers/Email/email.xml | 0 .../{fixtures => Fixtures}/Parsers/Extension/extension.json | 0 .../{fixtures => Fixtures}/Parsers/Extension/extension.xml | 0 tests/{fixtures => Fixtures}/Parsers/Link/link.json | 0 tests/{fixtures => Fixtures}/Parsers/Link/link.xml | 0 tests/{fixtures => Fixtures}/Parsers/Person/person.json | 0 tests/{fixtures => Fixtures}/Parsers/Person/person.xml | 0 tests/{fixtures => Fixtures}/basic.gpx | 0 tests/{fixtures => Fixtures}/gps-track.gpx | 0 tests/{fixtures => Fixtures}/hiking.gpx | 0 tests/{fixtures => Fixtures}/minimal.gpx | 0 tests/{fixtures => Fixtures}/route.gpx | 0 tests/{fixtures => Fixtures}/timezero.gpx | 0 20 files changed, 4 insertions(+), 4 deletions(-) rename tests/{fixtures => Fixtures}/Parsers/Bounds/bounds.json (100%) rename tests/{fixtures => Fixtures}/Parsers/Bounds/bounds.xml (100%) rename tests/{fixtures => Fixtures}/Parsers/Copyright/copyright.json (100%) rename tests/{fixtures => Fixtures}/Parsers/Copyright/copyright.xml (100%) rename tests/{fixtures => Fixtures}/Parsers/Email/email.json (100%) rename tests/{fixtures => Fixtures}/Parsers/Email/email.xml (100%) rename tests/{fixtures => Fixtures}/Parsers/Extension/extension.json (100%) rename tests/{fixtures => Fixtures}/Parsers/Extension/extension.xml (100%) rename tests/{fixtures => Fixtures}/Parsers/Link/link.json (100%) rename tests/{fixtures => Fixtures}/Parsers/Link/link.xml (100%) rename tests/{fixtures => Fixtures}/Parsers/Person/person.json (100%) rename tests/{fixtures => Fixtures}/Parsers/Person/person.xml (100%) rename tests/{fixtures => Fixtures}/basic.gpx (100%) rename tests/{fixtures => Fixtures}/gps-track.gpx (100%) rename tests/{fixtures => Fixtures}/hiking.gpx (100%) rename tests/{fixtures => Fixtures}/minimal.gpx (100%) rename tests/{fixtures => Fixtures}/route.gpx (100%) rename tests/{fixtures => Fixtures}/timezero.gpx (100%) diff --git a/composer.json b/composer.json index 079ad61..c6064fb 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-dom": "*" }, "require-dev": { - "phpunit/phpunit": "^12.2.6", + "phpunit/phpunit": "^11.0 || ^12.0", "friendsofphp/php-cs-fixer": "^v3.43.1" }, "autoload": { diff --git a/phpunit.xml b/phpunit.xml index 1e5d4dc..3e2499f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ - + src - + \ No newline at end of file diff --git a/tests/fixtures/Parsers/Bounds/bounds.json b/tests/Fixtures/Parsers/Bounds/bounds.json similarity index 100% rename from tests/fixtures/Parsers/Bounds/bounds.json rename to tests/Fixtures/Parsers/Bounds/bounds.json diff --git a/tests/fixtures/Parsers/Bounds/bounds.xml b/tests/Fixtures/Parsers/Bounds/bounds.xml similarity index 100% rename from tests/fixtures/Parsers/Bounds/bounds.xml rename to tests/Fixtures/Parsers/Bounds/bounds.xml diff --git a/tests/fixtures/Parsers/Copyright/copyright.json b/tests/Fixtures/Parsers/Copyright/copyright.json similarity index 100% rename from tests/fixtures/Parsers/Copyright/copyright.json rename to tests/Fixtures/Parsers/Copyright/copyright.json diff --git a/tests/fixtures/Parsers/Copyright/copyright.xml b/tests/Fixtures/Parsers/Copyright/copyright.xml similarity index 100% rename from tests/fixtures/Parsers/Copyright/copyright.xml rename to tests/Fixtures/Parsers/Copyright/copyright.xml diff --git a/tests/fixtures/Parsers/Email/email.json b/tests/Fixtures/Parsers/Email/email.json similarity index 100% rename from tests/fixtures/Parsers/Email/email.json rename to tests/Fixtures/Parsers/Email/email.json diff --git a/tests/fixtures/Parsers/Email/email.xml b/tests/Fixtures/Parsers/Email/email.xml similarity index 100% rename from tests/fixtures/Parsers/Email/email.xml rename to tests/Fixtures/Parsers/Email/email.xml diff --git a/tests/fixtures/Parsers/Extension/extension.json b/tests/Fixtures/Parsers/Extension/extension.json similarity index 100% rename from tests/fixtures/Parsers/Extension/extension.json rename to tests/Fixtures/Parsers/Extension/extension.json diff --git a/tests/fixtures/Parsers/Extension/extension.xml b/tests/Fixtures/Parsers/Extension/extension.xml similarity index 100% rename from tests/fixtures/Parsers/Extension/extension.xml rename to tests/Fixtures/Parsers/Extension/extension.xml diff --git a/tests/fixtures/Parsers/Link/link.json b/tests/Fixtures/Parsers/Link/link.json similarity index 100% rename from tests/fixtures/Parsers/Link/link.json rename to tests/Fixtures/Parsers/Link/link.json diff --git a/tests/fixtures/Parsers/Link/link.xml b/tests/Fixtures/Parsers/Link/link.xml similarity index 100% rename from tests/fixtures/Parsers/Link/link.xml rename to tests/Fixtures/Parsers/Link/link.xml diff --git a/tests/fixtures/Parsers/Person/person.json b/tests/Fixtures/Parsers/Person/person.json similarity index 100% rename from tests/fixtures/Parsers/Person/person.json rename to tests/Fixtures/Parsers/Person/person.json diff --git a/tests/fixtures/Parsers/Person/person.xml b/tests/Fixtures/Parsers/Person/person.xml similarity index 100% rename from tests/fixtures/Parsers/Person/person.xml rename to tests/Fixtures/Parsers/Person/person.xml diff --git a/tests/fixtures/basic.gpx b/tests/Fixtures/basic.gpx similarity index 100% rename from tests/fixtures/basic.gpx rename to tests/Fixtures/basic.gpx diff --git a/tests/fixtures/gps-track.gpx b/tests/Fixtures/gps-track.gpx similarity index 100% rename from tests/fixtures/gps-track.gpx rename to tests/Fixtures/gps-track.gpx diff --git a/tests/fixtures/hiking.gpx b/tests/Fixtures/hiking.gpx similarity index 100% rename from tests/fixtures/hiking.gpx rename to tests/Fixtures/hiking.gpx diff --git a/tests/fixtures/minimal.gpx b/tests/Fixtures/minimal.gpx similarity index 100% rename from tests/fixtures/minimal.gpx rename to tests/Fixtures/minimal.gpx diff --git a/tests/fixtures/route.gpx b/tests/Fixtures/route.gpx similarity index 100% rename from tests/fixtures/route.gpx rename to tests/Fixtures/route.gpx diff --git a/tests/fixtures/timezero.gpx b/tests/Fixtures/timezero.gpx similarity index 100% rename from tests/fixtures/timezero.gpx rename to tests/Fixtures/timezero.gpx From 6cc0b9cfdd13a16329e097761ac94b2ac89541e6 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Fri, 6 Mar 2026 03:33:07 +0100 Subject: [PATCH 11/31] Broaden PHPUnit version support and update configuration schema to 10.5. --- composer.json | 2 +- phpunit.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c6064fb..33b709c 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-dom": "*" }, "require-dev": { - "phpunit/phpunit": "^11.0 || ^12.0", + "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0", "friendsofphp/php-cs-fixer": "^v3.43.1" }, "autoload": { diff --git a/phpunit.xml b/phpunit.xml index 3e2499f..d49233d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ - + src From d3b6b9ed0ba7c75ed9c39e178559b6f14cc7aafc Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Fri, 6 Mar 2026 03:36:35 +0100 Subject: [PATCH 12/31] Set `fail-fast` to false in CI matrix configuration --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f2aaf0..ff529a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: php-version: ['8.1', '8.2', '8.3', '8.4'] steps: From f330cc2e6464dbabbe320dae3d83f47d3e5989a1 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Fri, 6 Mar 2026 03:44:40 +0100 Subject: [PATCH 13/31] Standardize test fixture directory naming convention to align with capitalization style. --- tests/Integration/GeoJsonOutputTest.php | 2 +- tests/Integration/GpxFileLoadTest.php | 2 +- tests/Integration/XmlRoundTripTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php index ebd3f56..bd749ab 100644 --- a/tests/Integration/GeoJsonOutputTest.php +++ b/tests/Integration/GeoJsonOutputTest.php @@ -12,7 +12,7 @@ class GeoJsonOutputTest extends TestCase { - private const FIXTURES_DIR = __DIR__ . '/../fixtures'; + private const FIXTURES_DIR = __DIR__ . '/../Fixtures'; public function testGpxFileJsonSerializeIsFeatureCollection(): void { diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php index 198b3e6..edff0ae 100644 --- a/tests/Integration/GpxFileLoadTest.php +++ b/tests/Integration/GpxFileLoadTest.php @@ -7,7 +7,7 @@ class GpxFileLoadTest extends TestCase { - private const FIXTURES_DIR = __DIR__ . '/../fixtures'; + private const FIXTURES_DIR = __DIR__ . '/../Fixtures'; public function testLoadTimezeroGpx(): void { diff --git a/tests/Integration/XmlRoundTripTest.php b/tests/Integration/XmlRoundTripTest.php index 4053c5c..9b78efc 100644 --- a/tests/Integration/XmlRoundTripTest.php +++ b/tests/Integration/XmlRoundTripTest.php @@ -7,7 +7,7 @@ class XmlRoundTripTest extends TestCase { - private const FIXTURES_DIR = __DIR__ . '/../fixtures'; + private const FIXTURES_DIR = __DIR__ . '/../Fixtures'; /** * Load a GPX file, serialize to XML, parse again, and verify key data is preserved. From 13d69c46753e68c95d9d75004537797959da58c1 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 15:31:27 +0100 Subject: [PATCH 14/31] =?UTF-8?q?Replaced=20`toArray()`=20with=20`jsonSeri?= =?UTF-8?q?alize()`=20across=20models=20and=20tests,=20removed=20redundant?= =?UTF-8?q?=20serialization=20methods,=20and=20standardized=20JSON=20outpu?= =?UTF-8?q?t=20structure=20=F0=9F=AB=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/phpGPX/Helpers/SerializationHelper.php | 80 ++------- src/phpGPX/Models/Copyright.php | 24 +-- src/phpGPX/Models/Email.php | 21 +-- src/phpGPX/Models/Extensions.php | 22 +-- .../Models/Extensions/TrackPointExtension.php | 21 +-- src/phpGPX/Models/GpxFile.php | 64 ++----- src/phpGPX/Models/Link.php | 23 +-- src/phpGPX/Models/Metadata.php | 36 ++-- src/phpGPX/Models/Person.php | 24 +-- src/phpGPX/Models/Point.php | 112 +++---------- src/phpGPX/Models/Route.php | 126 ++++++-------- src/phpGPX/Models/Segment.php | 114 +++++-------- src/phpGPX/Models/Stats.php | 41 ++--- src/phpGPX/Models/Summarizable.php | 0 src/phpGPX/Models/Track.php | 157 +++++------------- .../Fixtures/Parsers/Extension/extension.json | 13 +- tests/Integration/GeoJsonOutputTest.php | 15 +- .../Unit/Helpers/SerializationHelperTest.php | 79 +-------- tests/Unit/Models/StatsCalculationTest.php | 27 +-- tests/Unit/Parsers/CopyrightParserTest.php | 4 +- tests/Unit/Parsers/EmailParserTest.php | 4 +- tests/Unit/Parsers/ExtensionParserTest.php | 8 +- tests/Unit/Parsers/LinkParserTest.php | 4 +- tests/Unit/Parsers/PersonParserTest.php | 8 +- 24 files changed, 272 insertions(+), 755 deletions(-) delete mode 100644 src/phpGPX/Models/Summarizable.php diff --git a/src/phpGPX/Helpers/SerializationHelper.php b/src/phpGPX/Helpers/SerializationHelper.php index 21736c7..4a9df7f 100644 --- a/src/phpGPX/Helpers/SerializationHelper.php +++ b/src/phpGPX/Helpers/SerializationHelper.php @@ -13,77 +13,19 @@ */ abstract class SerializationHelper { - - /** - * Returns integer or null. - * @param $value - * @return int|null - */ - public static function integerOrNull($value): ?int - { - return is_numeric($value) ? (integer) $value : null; - } - - /** - * Returns float or null. - * @param $value - * @return float|null - */ - public static function floatOrNull($value): ?float - { - return is_numeric($value) ? (float) $value : null; - } - /** - * Returns string or null - * @param $value - * @return null|string + * Build a GeoJSON position array [lon, lat] or [lon, lat, ele]. + * @param float|null $longitude + * @param float|null $latitude + * @param float|null $elevation + * @return array */ - public static function stringOrNull($value): ?string - { - return is_string($value) ? $value : null; - } - - /** - * Recursively traverse objects and returns their array representation. - * If the object has a toArray method, it will be used, otherwise jsonSerialize will be used. - * @param \JsonSerializable|array|null $object - * @return array|null - */ - public static function serialize(\JsonSerializable|array|null $object): ?array - { - if (is_array($object)) { - $result = []; - foreach ($object as $record) { - if (method_exists($record, 'toArray')) { - $result[] = $record->toArray(); - } else { - $result[] = $record->jsonSerialize(); - } - $record = null; - } - $object = null; - return $result; - } else { - if ($object !== null && method_exists($object, 'toArray')) { - return $object->toArray(); - } - return $object?->jsonSerialize(); + public static function position(?float $longitude, ?float $latitude, ?float $elevation = null): array + { + $pos = [(float) $longitude, (float) $latitude]; + if ($elevation !== null) { + $pos[] = $elevation; } - } - - public static function filterNotNull(array $array): array - { - foreach ($array as &$item) { - if (!is_array($item)) { - continue; - } - - $item = self::filterNotNull($item); - } - - return array_filter($array, function ($item) { - return $item !== null && (!is_array($item) || count($item)); - }); + return $pos; } } diff --git a/src/phpGPX/Models/Copyright.php b/src/phpGPX/Models/Copyright.php index fff25fb..a56c9ed 100644 --- a/src/phpGPX/Models/Copyright.php +++ b/src/phpGPX/Models/Copyright.php @@ -7,7 +7,6 @@ namespace phpGPX\Models; use phpGPX\GpxSerializable; -use phpGPX\Helpers\SerializationHelper; /** * Class Copyright @@ -47,26 +46,13 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'author' => $this->author, - 'year' => SerializationHelper::stringOrNull($this->year), - 'license' => SerializationHelper::stringOrNull($this->license) - ]; - } - - /** - * Implements JsonSerializable interface - * @return array - */ public function jsonSerialize(): array { - return $this->toArray(); + return array_filter([ + 'author' => $this->author, + 'year' => $this->year, + 'license' => $this->license, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Email.php b/src/phpGPX/Models/Email.php index c529dff..64d93b7 100644 --- a/src/phpGPX/Models/Email.php +++ b/src/phpGPX/Models/Email.php @@ -37,25 +37,12 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'id' => $this->id !== null ? (string) $this->id : null, - 'domain' => $this->domain !== null ? (string) $this->domain : null - ]; - } - - /** - * Serialize object to array for JSON encoding - * @return array - */ public function jsonSerialize(): array { - return $this->toArray(); + return array_filter([ + 'id' => $this->id, + 'domain' => $this->domain, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Extensions.php b/src/phpGPX/Models/Extensions.php index 11cca7c..c1cbd3e 100644 --- a/src/phpGPX/Models/Extensions.php +++ b/src/phpGPX/Models/Extensions.php @@ -7,7 +7,6 @@ namespace phpGPX\Models; use phpGPX\GpxSerializable; -use phpGPX\Helpers\SerializationHelper; use phpGPX\Models\Extensions\TrackPointExtension; /** @@ -37,25 +36,12 @@ public function __construct() $this->trackPointExtension = null; } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'trackpoint' => SerializationHelper::serialize($this->trackPointExtension), - 'unsupported' => $this->unsupported, - ]; - } - - /** - * Implements JsonSerializable interface - * @return array - */ public function jsonSerialize(): array { - return $this->toArray(); + return array_filter([ + 'trackpoint' => $this->trackPointExtension, + 'unsupported' => !empty($this->unsupported) ? $this->unsupported : null, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php index 09f3424..02da3b3 100644 --- a/src/phpGPX/Models/Extensions/TrackPointExtension.php +++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php @@ -79,13 +79,9 @@ public function __construct() parent::__construct(self::EXTENSION_NAMESPACE, self::EXTENSION_NAME); } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array + public function jsonSerialize(): array { - return [ + return array_filter([ 'aTemp' => $this->aTemp ?? null, 'wTemp' => $this->wTemp ?? null, 'depth' => $this->depth ?? null, @@ -93,16 +89,7 @@ public function toArray(): array 'cad' => $this->cad ?? null, 'speed' => $this->speed ?? null, 'course' => $this->course ?? null, - 'bearing' => $this->bearing ?? null - ]; - } - - /** - * Serialize object to array for JSON encoding - * @return array - */ - public function jsonSerialize(): array - { - return $this->toArray(); + 'bearing' => $this->bearing ?? null, + ], fn($v) => $v !== null); } } diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php index c4211f6..185021d 100644 --- a/src/phpGPX/Models/GpxFile.php +++ b/src/phpGPX/Models/GpxFile.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\Helpers\SerializationHelper; use phpGPX\Parsers\ExtensionParser; use phpGPX\Parsers\MetadataParser; use phpGPX\Parsers\PointParser; @@ -71,52 +70,36 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return SerializationHelper::filterNotNull([ - 'creator' => SerializationHelper::stringOrNull($this->creator), - 'metadata' => SerializationHelper::serialize($this->metadata), - 'waypoints' => SerializationHelper::serialize($this->waypoints), - 'routes' => SerializationHelper::serialize($this->routes), - 'tracks' => SerializationHelper::serialize($this->tracks), - 'extensions' => SerializationHelper::serialize($this->extensions) - ]); - } - - /** - * Serialize object to array for JSON encoding - * Always returns GeoJSON format - * @return array - */ public function jsonSerialize(): array { - // GeoJSON FeatureCollection format $features = []; - // Add waypoints as Point features - each waypoint handles its own serialization foreach ($this->waypoints as $waypoint) { - $features[] = $waypoint->jsonSerialize(); + $features[] = $waypoint; } - // Add routes as LineString features - each route handles its own serialization foreach ($this->routes as $route) { - $features[] = $route->jsonSerialize(); + $features[] = $route; } - // Add tracks as MultiLineString features - each track handles its own serialization foreach ($this->tracks as $track) { - $features[] = $track->jsonSerialize(); + $features[] = $track; } - return [ + $result = [ 'type' => 'FeatureCollection', 'features' => $features, - 'metadata' => SerializationHelper::serialize($this->metadata) ]; + + if ($this->metadata !== null) { + $result['properties'] = array_filter([ + 'metadata' => $this->metadata, + 'creator' => $this->creator, + 'extensions' => $this->extensions, + ], fn($v) => $v !== null); + } + + return $result; } /** @@ -145,19 +128,12 @@ public function gpxDeserialize(\DOMDocument &$document): void /** - * Return JSON representation of GPX file with statistics. - * @param bool $geojson Whether to return GeoJSON format (true) or GPX format (false) + * Return GeoJSON representation of GPX file. * @return string */ - public function toJSON(bool $geojson = true): string + public function toJSON(): string { - if ($geojson) { - // GeoJSON format (using jsonSerialize) - return json_encode($this->jsonSerialize(), phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : null); - } else { - // GPX format (using toArray) - return json_encode($this->toArray(), phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : null); - } + return json_encode($this, phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : 0); } /** @@ -239,12 +215,8 @@ public function save(string $path, string $format): void $document->save($path); break; case phpGPX::JSON_FORMAT: - // Use GPX format for JSON - file_put_contents($path, $this->toJSON(false)); - break; case phpGPX::GEOJSON_FORMAT: - // Use GeoJSON format - file_put_contents($path, $this->toJSON(true)); + file_put_contents($path, $this->toJSON()); break; default: throw new \RuntimeException("Unsupported file format!"); diff --git a/src/phpGPX/Models/Link.php b/src/phpGPX/Models/Link.php index df09259..28aec5c 100644 --- a/src/phpGPX/Models/Link.php +++ b/src/phpGPX/Models/Link.php @@ -46,26 +46,13 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'href' => $this->href !== null ? (string) $this->href : null, - 'text' => $this->text, - 'type' => $this->type - ]; - } - - /** - * Implements JsonSerializable interface - * @return array - */ public function jsonSerialize(): array { - return $this->toArray(); + return array_filter([ + 'href' => $this->href, + 'text' => $this->text, + 'type' => $this->type, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Metadata.php b/src/phpGPX/Models/Metadata.php index 05ac2d7..916cba1 100644 --- a/src/phpGPX/Models/Metadata.php +++ b/src/phpGPX/Models/Metadata.php @@ -8,7 +8,6 @@ use phpGPX\GpxSerializable; use phpGPX\Helpers\DateTimeHelper; -use phpGPX\Helpers\SerializationHelper; /** * Class Metadata @@ -95,32 +94,19 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'name' => SerializationHelper::stringOrNull($this->name), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'author' => SerializationHelper::serialize($this->author), - 'copyright' => SerializationHelper::serialize($this->copyright), - 'links' => SerializationHelper::serialize($this->links), - 'time' => DateTimeHelper::formatDateTime($this->time), - 'keywords' => SerializationHelper::stringOrNull($this->keywords), - 'bounds' => SerializationHelper::serialize($this->bounds), - 'extensions' => SerializationHelper::serialize($this->extensions) - ]; - } - - /** - * Implements JsonSerializable interface - * @return array - */ public function jsonSerialize(): array { - return $this->toArray(); + return array_filter([ + 'name' => $this->name, + 'desc' => $this->description, + 'author' => $this->author, + 'copyright' => $this->copyright, + 'links' => !empty($this->links) ? $this->links : null, + 'time' => DateTimeHelper::formatDateTime($this->time), + 'keywords' => $this->keywords, + 'bounds' => $this->bounds, + 'extensions' => $this->extensions, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Person.php b/src/phpGPX/Models/Person.php index 2709121..d75ccc1 100644 --- a/src/phpGPX/Models/Person.php +++ b/src/phpGPX/Models/Person.php @@ -7,7 +7,6 @@ namespace phpGPX\Models; use phpGPX\GpxSerializable; -use phpGPX\Helpers\SerializationHelper; /** * Class Person @@ -49,26 +48,13 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'name' => (string) $this->name, - 'email' => SerializationHelper::serialize($this->email), - 'links' => SerializationHelper::serialize($this->links) - ]; - } - - /** - * Implements JsonSerializable interface - * @return array - */ public function jsonSerialize(): array { - return $this->toArray(); + return array_filter([ + 'name' => $this->name, + 'email' => $this->email, + 'links' => !empty($this->links) ? $this->links : null, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index 0909a7b..a51e606 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -6,8 +6,8 @@ namespace phpGPX\Models; -use phpGPX\Helpers\SerializationHelper; use phpGPX\Helpers\DateTimeHelper; +use phpGPX\Helpers\SerializationHelper; use phpGPX\phpGPX; enum PointType: string @@ -242,111 +242,45 @@ public function getPointType(): string return $this->pointType; } - /** - * Serialize object to array for JSON encoding - * Always returns GeoJSON format - * @return array - */ public function jsonSerialize(): array { - // GeoJSON Point format - $properties = [ - 'ele' => SerializationHelper::floatOrNull($this->elevation), + $properties = array_filter([ + 'name' => $this->name, + 'ele' => $this->elevation, 'time' => DateTimeHelper::formatDateTime($this->time, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), - 'magvar' => SerializationHelper::floatOrNull($this->magVar), - 'geoidheight' => SerializationHelper::floatOrNull($this->geoidHeight), - 'name' => SerializationHelper::stringOrNull($this->name), - 'cmt' => SerializationHelper::stringOrNull($this->comment), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'src' => SerializationHelper::stringOrNull($this->source), - 'link' => SerializationHelper::serialize($this->links), - 'sym' => SerializationHelper::stringOrNull($this->symbol), - 'type' => SerializationHelper::stringOrNull($this->type), - 'fix' => SerializationHelper::stringOrNull($this->fix), - 'sat' => SerializationHelper::integerOrNull($this->satellitesNumber), - 'hdop' => SerializationHelper::floatOrNull($this->hdop), - 'vdop' => SerializationHelper::floatOrNull($this->vdop), - 'pdop' => SerializationHelper::floatOrNull($this->pdop), - 'ageofdgpsdata' => SerializationHelper::floatOrNull($this->ageOfGpsData), - 'dgpsid' => SerializationHelper::integerOrNull($this->dgpsid), - 'difference' => SerializationHelper::floatOrNull($this->difference), - 'distance' => SerializationHelper::floatOrNull($this->distance), - 'extensions' => SerializationHelper::serialize($this->extensions) - ]; - - // Filter out null values - $properties = array_filter($properties, function ($value) { - return $value !== null; - }); + 'magvar' => $this->magVar, + 'geoidheight' => $this->geoidHeight, + 'cmt' => $this->comment, + 'desc' => $this->description, + 'src' => $this->source, + 'link' => !empty($this->links) ? $this->links : null, + 'sym' => $this->symbol, + 'type' => $this->type, + 'fix' => $this->fix, + 'sat' => $this->satellitesNumber, + 'hdop' => $this->hdop, + 'vdop' => $this->vdop, + 'pdop' => $this->pdop, + 'ageofdgpsdata' => $this->ageOfGpsData, + 'dgpsid' => $this->dgpsid, + 'extensions' => $this->extensions, + ], fn($v) => $v !== null); return [ 'type' => 'Feature', 'geometry' => [ 'type' => 'Point', - 'coordinates' => [ - (float) $this->longitude, - (float) $this->latitude, - SerializationHelper::floatOrNull($this->elevation) - ] + 'coordinates' => SerializationHelper::position($this->longitude, $this->latitude, $this->elevation), ], - 'properties' => $properties + 'properties' => $properties ?: new \stdClass(), ]; } - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ public static function gpxSerialize(\SimpleXMLElement $node): void { - // Implementation of GpxSerializable interface - // This method would be called to serialize a Point to GPX XML - // Since PointParser already handles this, this method can be empty } - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ public function gpxDeserialize(\DOMDocument &$document): void { - // Implementation of GpxSerializable interface - // This method would be called to deserialize GPX XML to a Point - // Since PointParser already handles this, this method can be empty - } - - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'lat' => (float) $this->latitude, - 'lon' => (float) $this->longitude, - 'ele' => SerializationHelper::floatOrNull($this->elevation), - 'time' => DateTimeHelper::formatDateTime($this->time, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), - 'magvar' => SerializationHelper::floatOrNull($this->magVar), - 'geoidheight' => SerializationHelper::floatOrNull($this->geoidHeight), - 'name' => SerializationHelper::stringOrNull($this->name), - 'cmt' => SerializationHelper::stringOrNull($this->comment), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'src' => SerializationHelper::stringOrNull($this->source), - 'link' => SerializationHelper::serialize($this->links), - 'sym' => SerializationHelper::stringOrNull($this->symbol), - 'type' => SerializationHelper::stringOrNull($this->type), - 'fix' => SerializationHelper::stringOrNull($this->fix), - 'sat' => SerializationHelper::integerOrNull($this->satellitesNumber), - 'hdop' => SerializationHelper::floatOrNull($this->hdop), - 'vdop' => SerializationHelper::floatOrNull($this->vdop), - 'pdop' => SerializationHelper::floatOrNull($this->pdop), - 'ageofdgpsdata' => SerializationHelper::floatOrNull($this->ageOfGpsData), - 'dgpsid' => SerializationHelper::integerOrNull($this->dgpsid), - 'difference' => SerializationHelper::floatOrNull($this->difference), - 'distance' => SerializationHelper::floatOrNull($this->distance), - 'extensions' => SerializationHelper::serialize($this->extensions) - ]; } } diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 8a86ad5..52fbd86 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -8,7 +8,6 @@ use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\ElevationGainLossCalculator; -use phpGPX\Helpers\GeoHelper; use phpGPX\Helpers\SerializationHelper; use phpGPX\phpGPX; @@ -54,52 +53,32 @@ public function getPoints(): array return $points; } - /** - * Serialize object to array for JSON encoding - * Always returns GeoJSON format - * @return array - */ public function jsonSerialize(): array { - // GeoJSON LineString feature $coordinates = []; - $properties = [ - 'name' => SerializationHelper::stringOrNull($this->name), - 'cmt' => SerializationHelper::stringOrNull($this->comment), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'src' => SerializationHelper::stringOrNull($this->source), - 'link' => SerializationHelper::serialize($this->links), - 'number' => SerializationHelper::integerOrNull($this->number), - 'type' => SerializationHelper::stringOrNull($this->type), - 'extensions' => SerializationHelper::serialize($this->extensions) - ]; - - // Filter out null values - $properties = array_filter($properties, function ($value) { - return $value !== null; - }); - - // Add stats if available - if ($this->stats) { - $properties['stats'] = $this->stats->jsonSerialize(); - } - - // Collect coordinates from route points foreach ($this->points as $point) { - $coordinates[] = [ - (float) $point->longitude, - (float) $point->latitude, - SerializationHelper::floatOrNull($point->elevation) - ]; + $coordinates[] = SerializationHelper::position($point->longitude, $point->latitude, $point->elevation); } + $properties = array_filter([ + 'name' => $this->name, + 'cmt' => $this->comment, + 'desc' => $this->description, + 'src' => $this->source, + 'link' => !empty($this->links) ? $this->links : null, + 'number' => $this->number, + 'type' => $this->type, + 'extensions' => $this->extensions, + 'stats' => $this->stats, + ], fn($v) => $v !== null); + return [ 'type' => 'Feature', 'geometry' => [ 'type' => 'LineString', - 'coordinates' => $coordinates + 'coordinates' => $coordinates, ], - 'properties' => $properties + 'properties' => $properties ?: new \stdClass(), ]; } @@ -127,26 +106,6 @@ public function gpxDeserialize(\DOMDocument &$document): void // Since RouteParser already handles this, this method can be empty } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'name' => SerializationHelper::stringOrNull($this->name), - 'cmt' => SerializationHelper::stringOrNull($this->comment), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'src' => SerializationHelper::stringOrNull($this->source), - 'link' => SerializationHelper::serialize($this->links), - 'number' => SerializationHelper::integerOrNull($this->number), - 'type' => SerializationHelper::stringOrNull($this->type), - 'extensions' => SerializationHelper::serialize($this->extensions), - 'rtep' => SerializationHelper::serialize($this->points), - 'stats' => SerializationHelper::serialize($this->stats) - ]; - } - /** * Recalculate stats objects. * @return void @@ -165,16 +124,6 @@ public function recalculateStats(): void $pointCount = count($this->points); - $firstPoint = &$this->points[0]; - $lastPoint = end($this->points); - - $this->stats->startedAt = $firstPoint->time; - $this->stats->startedAtCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - $this->stats->finishedAt = $lastPoint->time; - $this->stats->finishedAtCoords = ["lat" => $lastPoint->latitude, "lng" => $lastPoint->longitude]; - $this->stats->minAltitude = $firstPoint->elevation; - $this->stats->minAltitudeCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - list($this->stats->cumulativeElevationGain, $this->stats->cumulativeElevationLoss) = ElevationGainLossCalculator::calculate($this->getPoints()); @@ -182,25 +131,46 @@ public function recalculateStats(): void $this->stats->distance = $calculator->getRawDistance(); $this->stats->realDistance = $calculator->getRealDistance(); - for ($p = 0; $p < $pointCount; $p++) { - if ((phpGPX::$IGNORE_ELEVATION_0 === false || $this->points[$p]->elevation > 0) && $this->stats->minAltitude > $this->points[$p]->elevation) { - $this->stats->minAltitude = $this->points[$p]->elevation; - $this->stats->minAltitudeCoords = ["lat" => $this->points[$p]->latitude, "lng" => $this->points[$p]->longitude]; + // Find first/last non-null timestamps (#51) + for ($i = 0; $i < $pointCount; $i++) { + if ($this->points[$i]->time instanceof \DateTime) { + $this->stats->startedAt = $this->points[$i]->time; + $this->stats->startedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + break; } + } + for ($i = $pointCount - 1; $i >= 0; $i--) { + if ($this->points[$i]->time instanceof \DateTime) { + $this->stats->finishedAt = $this->points[$i]->time; + $this->stats->finishedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + break; + } + } - if ($this->stats->maxAltitude < $this->points[$p]->elevation) { - $this->stats->maxAltitude = $this->points[$p]->elevation; - $this->stats->maxAltitudeCoords = ["lat" => $this->points[$p]->latitude, "lng" => $this->points[$p]->longitude]; + // Find min/max altitude — don't assume first point (#70) + for ($i = 0; $i < $pointCount; $i++) { + $ele = $this->points[$i]->elevation; + if ($ele === null) { + continue; + } + if (phpGPX::$IGNORE_ELEVATION_0 && $ele == 0) { + continue; } - if ($this->stats->minAltitude > $this->points[$p]->elevation) { - $this->stats->minAltitude = $this->points[$p]->elevation; - $this->stats->minAltitudeCoords = ["lat" => $this->points[$p]->latitude, "lng" => $this->points[$p]->longitude]; + $coords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + + if ($this->stats->maxAltitude === null || $ele > $this->stats->maxAltitude) { + $this->stats->maxAltitude = $ele; + $this->stats->maxAltitudeCoords = $coords; + } + if ($this->stats->minAltitude === null || $ele < $this->stats->minAltitude) { + $this->stats->minAltitude = $ele; + $this->stats->minAltitudeCoords = $coords; } } - if (($firstPoint->time instanceof \DateTime) && ($lastPoint->time instanceof \DateTime)) { - $this->stats->duration = $lastPoint->time->getTimestamp() - $firstPoint->time->getTimestamp(); + if ($this->stats->startedAt instanceof \DateTime && $this->stats->finishedAt instanceof \DateTime) { + $this->stats->duration = $this->stats->finishedAt->getTimestamp() - $this->stats->startedAt->getTimestamp(); if ($this->stats->duration != 0) { $this->stats->averageSpeed = $this->stats->distance / $this->stats->duration; diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index b46b156..63d7077 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -9,7 +9,6 @@ use phpGPX\GpxSerializable; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\ElevationGainLossCalculator; -use phpGPX\Helpers\GeoHelper; use phpGPX\Helpers\SerializationHelper; use phpGPX\phpGPX; @@ -50,79 +49,34 @@ public function __construct() } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'points' => SerializationHelper::serialize($this->points), - 'extensions' => SerializationHelper::serialize($this->extensions), - 'stats' => SerializationHelper::serialize($this->stats) - ]; - } - - /** - * Implements JsonSerializable interface - * Always returns GeoJSON format - * @return array - */ public function jsonSerialize(): array { - // GeoJSON LineString feature $coordinates = []; - $properties = [ - 'extensions' => SerializationHelper::serialize($this->extensions) - ]; - - // Filter out null values - $properties = array_filter($properties, function ($value) { - return $value !== null; - }); - - // Add stats if available - if ($this->stats) { - $properties['stats'] = $this->stats->jsonSerialize(); - } - - // Collect coordinates from segment points foreach ($this->points as $point) { - $coordinates[] = [ - (float) $point->longitude, - (float) $point->latitude, - SerializationHelper::floatOrNull($point->elevation) - ]; + $coordinates[] = SerializationHelper::position($point->longitude, $point->latitude, $point->elevation); } + $properties = array_filter([ + 'extensions' => $this->extensions, + 'stats' => $this->stats, + ], fn($v) => $v !== null); + return [ 'type' => 'Feature', 'geometry' => [ 'type' => 'LineString', - 'coordinates' => $coordinates + 'coordinates' => $coordinates, ], - 'properties' => $properties + 'properties' => $properties ?: new \stdClass(), ]; } - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ public static function gpxSerialize(\SimpleXMLElement $node): void { - // Implementation required by GpxSerializable interface } - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ public function gpxDeserialize(\DOMDocument &$document): void { - // Implementation required by GpxSerializable interface } @@ -151,16 +105,6 @@ public function recalculateStats(): void return; } - $firstPoint = &$this->points[0]; - $lastPoint = end($this->points); - - $this->stats->startedAt = $firstPoint->time; - $this->stats->startedAtCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - $this->stats->finishedAt = $lastPoint->time; - $this->stats->finishedAtCoords = ["lat" => $lastPoint->latitude, "lng" => $lastPoint->longitude]; - $this->stats->minAltitude = $firstPoint->elevation; - $this->stats->minAltitudeCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - list($this->stats->cumulativeElevationGain, $this->stats->cumulativeElevationLoss) = ElevationGainLossCalculator::calculate($this->getPoints()); @@ -168,20 +112,46 @@ public function recalculateStats(): void $this->stats->distance = $calculator->getRawDistance(); $this->stats->realDistance = $calculator->getRealDistance(); + // Find first/last non-null timestamps (#51) + for ($i = 0; $i < $count; $i++) { + if ($this->points[$i]->time instanceof \DateTime) { + $this->stats->startedAt = $this->points[$i]->time; + $this->stats->startedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + break; + } + } + for ($i = $count - 1; $i >= 0; $i--) { + if ($this->points[$i]->time instanceof \DateTime) { + $this->stats->finishedAt = $this->points[$i]->time; + $this->stats->finishedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + break; + } + } + + // Find min/max altitude — don't assume first point (#70) for ($i = 0; $i < $count; $i++) { - if ($this->stats->maxAltitude < $this->points[$i]->elevation) { - $this->stats->maxAltitude = $this->points[$i]->elevation; - $this->stats->maxAltitudeCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + $ele = $this->points[$i]->elevation; + if ($ele === null) { + continue; + } + if (phpGPX::$IGNORE_ELEVATION_0 && $ele == 0) { + continue; } - if ((phpGPX::$IGNORE_ELEVATION_0 === false || $this->points[$i]->elevation > 0) && $this->stats->minAltitude > $this->points[$i]->elevation) { - $this->stats->minAltitude = $this->points[$i]->elevation; - $this->stats->minAltitudeCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + $coords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; + + if ($this->stats->maxAltitude === null || $ele > $this->stats->maxAltitude) { + $this->stats->maxAltitude = $ele; + $this->stats->maxAltitudeCoords = $coords; + } + if ($this->stats->minAltitude === null || $ele < $this->stats->minAltitude) { + $this->stats->minAltitude = $ele; + $this->stats->minAltitudeCoords = $coords; } } - if (isset($firstPoint->time) && isset($lastPoint->time) && $firstPoint->time instanceof \DateTime && $lastPoint->time instanceof \DateTime) { - $this->stats->duration = $lastPoint->time->getTimestamp() - $firstPoint->time->getTimestamp(); + if ($this->stats->startedAt instanceof \DateTime && $this->stats->finishedAt instanceof \DateTime) { + $this->stats->duration = $this->stats->finishedAt->getTimestamp() - $this->stats->startedAt->getTimestamp(); if ($this->stats->duration != 0) { $this->stats->averageSpeed = $this->stats->distance / $this->stats->duration; diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index 6a59e84..26de275 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -130,38 +130,25 @@ public function reset(): void $this->duration = null; } - /** - * Serialize object to array - * @return array - */ - public function toArray(): array + public function jsonSerialize(): array { - return [ - 'distance' => $this->distance !== null ? (float)$this->distance : null, - 'realDistance' => $this->realDistance !== null ? (float)$this->realDistance : null, - 'avgSpeed' => $this->averageSpeed !== null ? (float)$this->averageSpeed : null, - 'avgPace' => $this->averagePace !== null ? (float)$this->averagePace : null, - 'minAltitude' => $this->minAltitude !== null ? (float)$this->minAltitude : null, + return array_filter([ + 'distance' => $this->distance, + 'realDistance' => $this->realDistance, + 'avgSpeed' => $this->averageSpeed, + 'avgPace' => $this->averagePace, + 'minAltitude' => $this->minAltitude, 'minAltitudeCoords' => $this->minAltitudeCoords, - 'maxAltitude' => $this->maxAltitude !== null ? (float)$this->maxAltitude : null, + 'maxAltitude' => $this->maxAltitude, 'maxAltitudeCoords' => $this->maxAltitudeCoords, - 'cumulativeElevationGain' => $this->cumulativeElevationGain !== null ? (float)$this->cumulativeElevationGain : null, - 'cumulativeElevationLoss' => $this->cumulativeElevationLoss !== null ? (float)$this->cumulativeElevationLoss : null, - 'startedAt' => $this->startedAt !== null ? DateTimeHelper::formatDateTime($this->startedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT) : null, + 'cumulativeElevationGain' => $this->cumulativeElevationGain, + 'cumulativeElevationLoss' => $this->cumulativeElevationLoss, + 'startedAt' => DateTimeHelper::formatDateTime($this->startedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), 'startedAtCoords' => $this->startedAtCoords, - 'finishedAt' => $this->finishedAt !== null ? DateTimeHelper::formatDateTime($this->finishedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT) : null, + 'finishedAt' => DateTimeHelper::formatDateTime($this->finishedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), 'finishedAtCoords' => $this->finishedAtCoords, - 'duration' => $this->duration !== null ? (float)$this->duration : null - ]; - } - - /** - * Implements JsonSerializable interface - * @return array - */ - public function jsonSerialize(): array - { - return $this->toArray(); + 'duration' => $this->duration, + ], fn($v) => $v !== null); } /** diff --git a/src/phpGPX/Models/Summarizable.php b/src/phpGPX/Models/Summarizable.php deleted file mode 100644 index e69de29..0000000 diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index a7d8b5d..e6186ac 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\Helpers\GeoHelper; use phpGPX\Helpers\SerializationHelper; use phpGPX\phpGPX; @@ -53,103 +52,45 @@ public function getPoints(): array return $points; } - /** - * Serialize object to array for JSON encoding - * Always returns GeoJSON format - * @return array - */ public function jsonSerialize(): array { - // GeoJSON MultiLineString feature $segmentCoordinates = []; - $properties = [ - 'name' => SerializationHelper::stringOrNull($this->name), - 'cmt' => SerializationHelper::stringOrNull($this->comment), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'src' => SerializationHelper::stringOrNull($this->source), - 'link' => SerializationHelper::serialize($this->links), - 'number' => SerializationHelper::integerOrNull($this->number), - 'type' => SerializationHelper::stringOrNull($this->type), - 'extensions' => SerializationHelper::serialize($this->extensions) - ]; - - // Filter out null values - $properties = array_filter($properties, function ($value) { - return $value !== null; - }); - - // Add stats if available - if ($this->stats) { - $properties['stats'] = $this->stats->jsonSerialize(); - } - - // Collect coordinates from track segments foreach ($this->segments as $segment) { $coordinates = []; - foreach ($segment->points as $point) { - $coordinates[] = [ - (float) $point->longitude, - (float) $point->latitude, - SerializationHelper::floatOrNull($point->elevation) - ]; + $coordinates[] = SerializationHelper::position($point->longitude, $point->latitude, $point->elevation); } - $segmentCoordinates[] = $coordinates; } + $properties = array_filter([ + 'name' => $this->name, + 'cmt' => $this->comment, + 'desc' => $this->description, + 'src' => $this->source, + 'link' => !empty($this->links) ? $this->links : null, + 'number' => $this->number, + 'type' => $this->type, + 'extensions' => $this->extensions, + 'stats' => $this->stats, + ], fn($v) => $v !== null); + return [ 'type' => 'Feature', 'geometry' => [ 'type' => 'MultiLineString', - 'coordinates' => $segmentCoordinates + 'coordinates' => $segmentCoordinates, ], - 'properties' => $properties + 'properties' => $properties ?: new \stdClass(), ]; } - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ public static function gpxSerialize(\SimpleXMLElement $node): void { - // Implementation required by GpxSerializable interface - // This method would be called to serialize a Track to GPX XML - // Since TrackParser already handles this, this method can be empty } - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ public function gpxDeserialize(\DOMDocument &$document): void { - // Implementation required by GpxSerializable interface - // This method would be called to deserialize GPX XML to a Track - // Since TrackParser already handles this, this method can be empty - } - - /** - * Serialize object to array - * @return array - */ - public function toArray(): array - { - return [ - 'name' => SerializationHelper::stringOrNull($this->name), - 'cmt' => SerializationHelper::stringOrNull($this->comment), - 'desc' => SerializationHelper::stringOrNull($this->description), - 'src' => SerializationHelper::stringOrNull($this->source), - 'link' => SerializationHelper::serialize($this->links), - 'number' => SerializationHelper::integerOrNull($this->number), - 'type' => SerializationHelper::stringOrNull($this->type), - 'extensions' => SerializationHelper::serialize($this->extensions), - 'trkseg' => SerializationHelper::serialize($this->segments), - 'stats' => SerializationHelper::serialize($this->stats) - ]; } /** @@ -170,60 +111,38 @@ public function recalculateStats(): void $segmentsCount = count($this->segments); - $firstSegment = null; - $firstPoint = null; - - // Identify first Segment/Point - for ($s = 0; $s < $segmentsCount; $s++) { - $pointCount = count($this->segments[$s]->points); - for ($p = 0; $p < $pointCount; $p++) { - if (is_null($firstPoint)) { - $firstPoint = &$this->segments[$s]->points[$p]; - $firstSegment = &$this->segments[$s]; - break; - } - } - } - - if (empty($firstPoint)) { - return; - } - - $lastSegment = end($this->segments); - $lastPoint = end(end($this->segments)->points); - - $this->stats->startedAt = $firstPoint->time; - $this->stats->startedAtCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - $this->stats->finishedAt = $lastPoint->time; - $this->stats->finishedAtCoords = ["lat" => $lastPoint->latitude, "lng" => $lastPoint->longitude]; - $this->stats->minAltitude = $firstPoint->elevation; - $this->stats->minAltitudeCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - for ($s = 0; $s < $segmentsCount; $s++) { $this->segments[$s]->recalculateStats(); + $segStats = $this->segments[$s]->stats; - $this->stats->cumulativeElevationGain += $this->segments[$s]->stats->cumulativeElevationGain; - $this->stats->cumulativeElevationLoss += $this->segments[$s]->stats->cumulativeElevationLoss; - - $this->stats->distance += $this->segments[$s]->stats->distance; - $this->stats->realDistance += $this->segments[$s]->stats->realDistance; + $this->stats->cumulativeElevationGain += $segStats->cumulativeElevationGain; + $this->stats->cumulativeElevationLoss += $segStats->cumulativeElevationLoss; + $this->stats->distance += $segStats->distance; + $this->stats->realDistance += $segStats->realDistance; - if ($this->stats->minAltitude === null) { - $this->stats->minAltitude = $this->segments[$s]->stats->minAltitude; - $this->stats->minAltitudeCoords = $this->segments[$s]->stats->minAltitudeCoords; + // Aggregate min/max altitude from segments + if ($segStats->maxAltitude !== null && ($this->stats->maxAltitude === null || $segStats->maxAltitude > $this->stats->maxAltitude)) { + $this->stats->maxAltitude = $segStats->maxAltitude; + $this->stats->maxAltitudeCoords = $segStats->maxAltitudeCoords; } - if ($this->stats->maxAltitude < $this->segments[$s]->stats->maxAltitude) { - $this->stats->maxAltitude = $this->segments[$s]->stats->maxAltitude; - $this->stats->maxAltitudeCoords = $this->segments[$s]->stats->maxAltitudeCoords; + if ($segStats->minAltitude !== null && ($this->stats->minAltitude === null || $segStats->minAltitude < $this->stats->minAltitude)) { + $this->stats->minAltitude = $segStats->minAltitude; + $this->stats->minAltitudeCoords = $segStats->minAltitudeCoords; + } + + // Aggregate startedAt/finishedAt from segments (#51) + if ($segStats->startedAt instanceof \DateTime && ($this->stats->startedAt === null || $segStats->startedAt < $this->stats->startedAt)) { + $this->stats->startedAt = $segStats->startedAt; + $this->stats->startedAtCoords = $segStats->startedAtCoords; } - if ($this->stats->minAltitude > $this->segments[$s]->stats->minAltitude) { - $this->stats->minAltitude = $this->segments[$s]->stats->minAltitude; - $this->stats->minAltitudeCoords = $this->segments[$s]->stats->minAltitudeCoords; + if ($segStats->finishedAt instanceof \DateTime && ($this->stats->finishedAt === null || $segStats->finishedAt > $this->stats->finishedAt)) { + $this->stats->finishedAt = $segStats->finishedAt; + $this->stats->finishedAtCoords = $segStats->finishedAtCoords; } } - if (($firstPoint->time instanceof \DateTime) && ($lastPoint->time instanceof \DateTime)) { - $this->stats->duration = abs($lastPoint->time->getTimestamp() - $firstPoint->time->getTimestamp()); + if ($this->stats->startedAt instanceof \DateTime && $this->stats->finishedAt instanceof \DateTime) { + $this->stats->duration = abs($this->stats->finishedAt->getTimestamp() - $this->stats->startedAt->getTimestamp()); if ($this->stats->duration != 0) { $this->stats->averageSpeed = $this->stats->distance / $this->stats->duration; diff --git a/tests/Fixtures/Parsers/Extension/extension.json b/tests/Fixtures/Parsers/Extension/extension.json index 4cbd70b..4c3f1b7 100644 --- a/tests/Fixtures/Parsers/Extension/extension.json +++ b/tests/Fixtures/Parsers/Extension/extension.json @@ -1,13 +1,6 @@ { "trackpoint": { "aTemp": 14, - "wTemp": null, - "depth": null, - "hr": 152, - "cad": null, - "speed": null, - "course": null, - "bearing": null - }, - "unsupported": [] -} + "hr": 152 + } +} \ No newline at end of file diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php index bd749ab..8cc5f92 100644 --- a/tests/Integration/GeoJsonOutputTest.php +++ b/tests/Integration/GeoJsonOutputTest.php @@ -2,7 +2,6 @@ namespace phpGPX\Tests\Integration; -use phpGPX\Models\GpxFile; use phpGPX\Models\Point; use phpGPX\Models\Route; use phpGPX\Models\Segment; @@ -113,7 +112,7 @@ public function testTrackJsonIsMultiLineStringFeature(): void public function testLoadedFileGeoJsonStructure(): void { $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); - $json = $gpxFile->jsonSerialize(); + $json = json_decode(json_encode($gpxFile), true); $this->assertEquals('FeatureCollection', $json['type']); @@ -134,7 +133,7 @@ public function testLoadedFileGeoJsonStructure(): void public function testGeoJsonWithWaypoints(): void { $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/timezero.gpx'); - $json = $gpxFile->jsonSerialize(); + $json = json_decode(json_encode($gpxFile), true); $this->assertEquals('FeatureCollection', $json['type']); @@ -154,16 +153,10 @@ public function testToJsonOutput(): void { $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); - // GeoJSON format - $geoJson = $gpxFile->toJSON(true); + $geoJson = $gpxFile->toJSON(); $decoded = json_decode($geoJson, true); $this->assertNotNull($decoded); $this->assertEquals('FeatureCollection', $decoded['type']); - - // GPX array format - $gpxJson = $gpxFile->toJSON(false); - $decoded = json_decode($gpxJson, true); - $this->assertNotNull($decoded); - $this->assertArrayHasKey('routes', $decoded); + $this->assertArrayHasKey('features', $decoded); } } \ No newline at end of file diff --git a/tests/Unit/Helpers/SerializationHelperTest.php b/tests/Unit/Helpers/SerializationHelperTest.php index b125272..314804b 100644 --- a/tests/Unit/Helpers/SerializationHelperTest.php +++ b/tests/Unit/Helpers/SerializationHelperTest.php @@ -7,82 +7,21 @@ class SerializationHelperTest extends TestCase { - public function testIntegerOrNull(): void + public function testPositionWithElevation(): void { - $this->assertNull(SerializationHelper::integerOrNull("")); - $this->assertNull(SerializationHelper::integerOrNull(null)); - $this->assertNull(SerializationHelper::integerOrNull("BLA")); - $this->assertIsInt(SerializationHelper::integerOrNull(5)); - $this->assertIsInt(SerializationHelper::integerOrNull("5")); + $pos = SerializationHelper::position(9.860, 54.932, 100.5); + $this->assertEquals([9.860, 54.932, 100.5], $pos); } - public function testFloatOrNull(): void + public function testPositionWithoutElevation(): void { - $this->assertNull(SerializationHelper::floatOrNull("")); - $this->assertNull(SerializationHelper::floatOrNull(null)); - $this->assertNull(SerializationHelper::floatOrNull("BLA")); - $this->assertIsFloat(SerializationHelper::floatOrNull(5.6)); - $this->assertIsFloat(SerializationHelper::floatOrNull(5)); - $this->assertIsFloat(SerializationHelper::floatOrNull("5.6")); - $this->assertIsFloat(SerializationHelper::floatOrNull("5")); + $pos = SerializationHelper::position(9.860, 54.932); + $this->assertEquals([9.860, 54.932], $pos); } - public function testStringOrNull(): void + public function testPositionWithNullElevation(): void { - $this->assertNull(SerializationHelper::stringOrNull(null)); - $this->assertIsString(SerializationHelper::stringOrNull("")); - $this->assertIsString(SerializationHelper::stringOrNull("Bla bla")); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderFilterNotNull')] - public function testFilterNotNull(array $expected, array $actual): void - { - $this->assertEquals($expected, SerializationHelper::filterNotNull($actual)); - } - - public static function dataProviderFilterNotNull(): array - { - return [ - 'numeric 1' => [ - [], - [null], - ], - 'numeric 2' => [ - [], - [null, [null]], - ], - 'numeric 3' => [ - [1 => 1], - [null, 1], - ], - 'numeric 4' => [ - [1 => 1, 3 => 2], - [null, 1, null, 2, null], - ], - 'numeric 5' => [ - [1 => 1, 3 => 2, 5 => [0 => 3, 2 => 4], 6 => 5], - [null, 1, null, 2, null, [3, null, 4], 5, null], - ], - 'associative 1' => [ - [], - ["foo" => null], - ], - 'associative 2' => [ - [], - ["foo" => null, ["bar" => null]], - ], - 'associative 3' => [ - ["bar" => 1], - ["foo" => null, "bar" => 1], - ], - 'associative 4' => [ - ["bar" => 1, "caw" => 2], - ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null], - ], - 'associative 5' => [ - ["bar" => 1, "caw" => 2, "ere" => ["foo" => 3, "baz" => 4], "moo" => 5], - ["foo" => null, "bar" => 1, "baz" => null, "caw" => 2, "doo" => null, "ere" => ["foo" => 3, "bar" => null, "baz" => 4], "moo" => 5, "boo" => null], - ], - ]; + $pos = SerializationHelper::position(9.860, 54.932, null); + $this->assertEquals([9.860, 54.932], $pos); } } \ No newline at end of file diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php index aeca7ac..bc83cfc 100644 --- a/tests/Unit/Models/StatsCalculationTest.php +++ b/tests/Unit/Models/StatsCalculationTest.php @@ -319,7 +319,7 @@ public function testStatsReset(): void $this->assertNull($stats->finishedAt); } - public function testStatsToArray(): void + public function testStatsJsonSerialize(): void { $stats = new Stats(); $stats->distance = 1000.0; @@ -330,23 +330,14 @@ public function testStatsToArray(): void $stats->maxAltitude = 200.0; $stats->duration = 400.0; - $array = $stats->toArray(); - - $this->assertEquals(1000.0, $array['distance']); - $this->assertEquals(1005.0, $array['realDistance']); - $this->assertEquals(2.5, $array['avgSpeed']); - $this->assertEquals(400.0, $array['avgPace']); - $this->assertEquals(100.0, $array['minAltitude']); - $this->assertEquals(200.0, $array['maxAltitude']); - $this->assertEquals(400.0, $array['duration']); - } - - public function testStatsJsonSerialize(): void - { - $stats = new Stats(); - $stats->distance = 500.0; - $json = $stats->jsonSerialize(); - $this->assertEquals($stats->toArray(), $json); + + $this->assertEquals(1000.0, $json['distance']); + $this->assertEquals(1005.0, $json['realDistance']); + $this->assertEquals(2.5, $json['avgSpeed']); + $this->assertEquals(400.0, $json['avgPace']); + $this->assertEquals(100.0, $json['minAltitude']); + $this->assertEquals(200.0, $json['maxAltitude']); + $this->assertEquals(400.0, $json['duration']); } } \ No newline at end of file diff --git a/tests/Unit/Parsers/CopyrightParserTest.php b/tests/Unit/Parsers/CopyrightParserTest.php index 571056e..08936aa 100644 --- a/tests/Unit/Parsers/CopyrightParserTest.php +++ b/tests/Unit/Parsers/CopyrightParserTest.php @@ -34,7 +34,7 @@ public function testParse(): void $this->assertEquals($this->copyright->license, $copyright->license); $this->assertEquals($this->copyright->year, $copyright->year); - $this->assertEquals($this->copyright->toArray(), $copyright->toArray()); + $this->assertEquals($this->copyright->jsonSerialize(), $copyright->jsonSerialize()); } public function testToXML(): void @@ -52,7 +52,7 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/copyright.json', json_encode($this->copyright->toArray()) + self::FIXTURES_DIR . '/copyright.json', json_encode($this->copyright->jsonSerialize()) ); } } \ No newline at end of file diff --git a/tests/Unit/Parsers/EmailParserTest.php b/tests/Unit/Parsers/EmailParserTest.php index 6d0f373..c2f7b0e 100644 --- a/tests/Unit/Parsers/EmailParserTest.php +++ b/tests/Unit/Parsers/EmailParserTest.php @@ -32,7 +32,7 @@ public function testParse(): void $this->assertEquals($this->email->id, $email->id); $this->assertEquals($this->email->domain, $email->domain); - $this->assertEquals($this->email->toArray(), $email->toArray()); + $this->assertEquals($this->email->jsonSerialize(), $email->jsonSerialize()); } public function testToXML(): void @@ -50,7 +50,7 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/email.json', json_encode($this->email->toArray()) + self::FIXTURES_DIR . '/email.json', json_encode($this->email->jsonSerialize()) ); } } \ No newline at end of file diff --git a/tests/Unit/Parsers/ExtensionParserTest.php b/tests/Unit/Parsers/ExtensionParserTest.php index a459202..fabf699 100644 --- a/tests/Unit/Parsers/ExtensionParserTest.php +++ b/tests/Unit/Parsers/ExtensionParserTest.php @@ -32,10 +32,12 @@ public function testParse(): void $this->assertEquals($this->extensions->unsupported, $extensions->unsupported); $this->assertEquals( - $this->extensions->trackPointExtension->toArray(), $extensions->trackPointExtension->toArray() + $this->extensions->trackPointExtension->jsonSerialize(), $extensions->trackPointExtension->jsonSerialize() ); - $this->assertEquals($this->extensions->toArray(), $extensions->toArray()); + $this->assertJsonStringEqualsJsonString( + json_encode($this->extensions), json_encode($extensions) + ); } public function testToXML(): void @@ -67,7 +69,7 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/extension.json', json_encode($this->extensions->toArray()) + self::FIXTURES_DIR . '/extension.json', json_encode($this->extensions->jsonSerialize()) ); } } \ No newline at end of file diff --git a/tests/Unit/Parsers/LinkParserTest.php b/tests/Unit/Parsers/LinkParserTest.php index fa9a2f4..f5f8d63 100644 --- a/tests/Unit/Parsers/LinkParserTest.php +++ b/tests/Unit/Parsers/LinkParserTest.php @@ -35,7 +35,7 @@ public function testParse(): void $this->assertEquals($this->link->text, $link->text); $this->assertEquals($this->link->type, $link->type); - $this->assertEquals($this->link->toArray(), $link->toArray()); + $this->assertEquals($this->link->jsonSerialize(), $link->jsonSerialize()); } public function testToXML(): void @@ -57,7 +57,7 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/link.json', json_encode($this->link->toArray()) + self::FIXTURES_DIR . '/link.json', json_encode($this->link->jsonSerialize()) ); } } \ No newline at end of file diff --git a/tests/Unit/Parsers/PersonParserTest.php b/tests/Unit/Parsers/PersonParserTest.php index d996bf6..b0f788f 100644 --- a/tests/Unit/Parsers/PersonParserTest.php +++ b/tests/Unit/Parsers/PersonParserTest.php @@ -52,9 +52,9 @@ public function testParse(): void $this->assertEquals($this->person->links[0]->text, $person->links[0]->text); $this->assertEquals($this->person->links[0]->href, $person->links[0]->href); - $this->assertEquals($this->person->toArray(), $person->toArray()); - $this->assertEquals($this->person->email->toArray(), $person->email->toArray()); - $this->assertEquals($this->person->links[0]->toArray(), $person->links[0]->toArray()); + $this->assertEquals($this->person->jsonSerialize(), $person->jsonSerialize()); + $this->assertEquals($this->person->email->jsonSerialize(), $person->email->jsonSerialize()); + $this->assertEquals($this->person->links[0]->jsonSerialize(), $person->links[0]->jsonSerialize()); } /** @@ -86,7 +86,7 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/person.json', json_encode($this->person->toArray()) + self::FIXTURES_DIR . '/person.json', json_encode($this->person->jsonSerialize()) ); } } \ No newline at end of file From ed0f383a8d3224a85bca62444352c79077e5e339 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 15:47:32 +0100 Subject: [PATCH 15/31] =?UTF-8?q?Getting=20rid=20of=20`GpxSerializable`=20?= =?UTF-8?q?interface=20bullshit=20idea=20=F0=9F=A4=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/phpGPX/GpxSerializable.php | 10 ------- src/phpGPX/Models/Bounds.php | 16 +---------- src/phpGPX/Models/Copyright.php | 24 +---------------- src/phpGPX/Models/Email.php | 24 +---------------- src/phpGPX/Models/Extensions.php | 23 +--------------- .../Models/Extensions/AbstractExtension.php | 24 +---------------- src/phpGPX/Models/GpxFile.php | 27 +------------------ src/phpGPX/Models/Link.php | 24 +---------------- src/phpGPX/Models/Metadata.php | 23 +--------------- src/phpGPX/Models/Person.php | 24 +---------------- src/phpGPX/Models/Point.php | 2 +- src/phpGPX/Models/Route.php | 26 +----------------- src/phpGPX/Models/Segment.php | 12 +-------- src/phpGPX/Models/Stats.php | 23 +--------------- src/phpGPX/Models/Track.php | 10 +------ 15 files changed, 14 insertions(+), 278 deletions(-) delete mode 100644 src/phpGPX/GpxSerializable.php diff --git a/src/phpGPX/GpxSerializable.php b/src/phpGPX/GpxSerializable.php deleted file mode 100644 index 92f6f8c..0000000 --- a/src/phpGPX/GpxSerializable.php +++ /dev/null @@ -1,10 +0,0 @@ - $this->license, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Email.php b/src/phpGPX/Models/Email.php index 64d93b7..32863e1 100644 --- a/src/phpGPX/Models/Email.php +++ b/src/phpGPX/Models/Email.php @@ -6,14 +6,12 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; - /** * Class Email * An email address. Broken into two parts (id and domain) to help prevent email harvesting. * @package phpGPX\Models */ -class Email implements \JsonSerializable, GpxSerializable +class Email implements \JsonSerializable { /** @@ -44,24 +42,4 @@ public function jsonSerialize(): array 'domain' => $this->domain, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Extensions.php b/src/phpGPX/Models/Extensions.php index c1cbd3e..1749929 100644 --- a/src/phpGPX/Models/Extensions.php +++ b/src/phpGPX/Models/Extensions.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; use phpGPX\Models\Extensions\TrackPointExtension; /** @@ -14,7 +13,7 @@ * TODO: http://www.garmin.com/xmlschemas/GpxExtensions/v3 * @package phpGPX\Models */ -class Extensions implements \JsonSerializable, GpxSerializable +class Extensions implements \JsonSerializable { /** * GPX Garmin TrackPointExtension v1 @@ -43,24 +42,4 @@ public function jsonSerialize(): array 'unsupported' => !empty($this->unsupported) ? $this->unsupported : null, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Extensions/AbstractExtension.php b/src/phpGPX/Models/Extensions/AbstractExtension.php index 4ca8138..b46ab2c 100644 --- a/src/phpGPX/Models/Extensions/AbstractExtension.php +++ b/src/phpGPX/Models/Extensions/AbstractExtension.php @@ -6,9 +6,7 @@ namespace phpGPX\Models\Extensions; -use phpGPX\GpxSerializable; - -abstract class AbstractExtension implements \JsonSerializable, GpxSerializable +abstract class AbstractExtension implements \JsonSerializable { /** @@ -33,24 +31,4 @@ public function __construct(string $namespace, string $extensionName) $this->namespace = $namespace; $this->extensionName = $extensionName; } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php index 185021d..58c033a 100644 --- a/src/phpGPX/Models/GpxFile.php +++ b/src/phpGPX/Models/GpxFile.php @@ -18,7 +18,7 @@ * Representation of GPX file. * @package phpGPX\Models */ -class GpxFile implements \JsonSerializable, \phpGPX\GpxSerializable +class GpxFile implements \JsonSerializable { /** * A list of waypoints. @@ -102,31 +102,6 @@ public function jsonSerialize(): array return $result; } - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation of GpxSerializable interface - // This method would be called to serialize a GpxFile to GPX XML - // Since the toXML method already handles this, this method can be empty - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation of GpxSerializable interface - // This method would be called to deserialize GPX XML to a GpxFile - // Since the parse method in phpGPX class already handles this, this method can be empty - } - - /** * Return GeoJSON representation of GPX file. * @return string diff --git a/src/phpGPX/Models/Link.php b/src/phpGPX/Models/Link.php index 28aec5c..59ffb62 100644 --- a/src/phpGPX/Models/Link.php +++ b/src/phpGPX/Models/Link.php @@ -6,15 +6,13 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; - /** * Class Link according to GPX 1.1 specification. * A link to an external resource (Web page, digital photo, video clip, etc) with additional information. * @see http://www.topografix.com/GPX/1/1/#type_linkType * @package phpGPX\Models */ -class Link implements \JsonSerializable, GpxSerializable +class Link implements \JsonSerializable { /** @@ -54,24 +52,4 @@ public function jsonSerialize(): array 'type' => $this->type, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Metadata.php b/src/phpGPX/Models/Metadata.php index 916cba1..bb3c9cd 100644 --- a/src/phpGPX/Models/Metadata.php +++ b/src/phpGPX/Models/Metadata.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; use phpGPX\Helpers\DateTimeHelper; /** @@ -15,7 +14,7 @@ * Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. * @package phpGPX\Models */ -class Metadata implements \JsonSerializable, GpxSerializable +class Metadata implements \JsonSerializable { /** @@ -108,24 +107,4 @@ public function jsonSerialize(): array 'extensions' => $this->extensions, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Person.php b/src/phpGPX/Models/Person.php index d75ccc1..a0819b5 100644 --- a/src/phpGPX/Models/Person.php +++ b/src/phpGPX/Models/Person.php @@ -6,14 +6,12 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; - /** * Class Person * A person or organisation * @package phpGPX\Models */ -class Person implements \JsonSerializable, GpxSerializable +class Person implements \JsonSerializable { /** @@ -56,24 +54,4 @@ public function jsonSerialize(): array 'links' => !empty($this->links) ? $this->links : null, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index a51e606..2c8dbc8 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -23,7 +23,7 @@ enum PointType: string * @see http://www.topografix.com/GPX/1/1/#type_wptType * @package phpGPX\Models */ -class Point implements \JsonSerializable, \phpGPX\GpxSerializable +class Point implements \JsonSerializable { const WAYPOINT = 'waypoint'; const TRACKPOINT = 'track'; diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 52fbd86..50b7d8b 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -15,7 +15,7 @@ * Class Route * @package phpGPX\Models */ -class Route extends Collection implements \phpGPX\GpxSerializable +class Route extends Collection { /** @@ -82,30 +82,6 @@ public function jsonSerialize(): array ]; } - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - // This method would be called to serialize a Route to GPX XML - // Since RouteParser already handles this, this method can be empty - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - // This method would be called to deserialize GPX XML to a Route - // Since RouteParser already handles this, this method can be empty - } - /** * Recalculate stats objects. * @return void diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index 63d7077..cea63e2 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Helpers\SerializationHelper; @@ -19,7 +18,7 @@ * start a new Track Segment for each continuous span of track data. * @package phpGPX\Models */ -class Segment implements \JsonSerializable, GpxSerializable, StatsCalculator +class Segment implements \JsonSerializable, StatsCalculator { /** * Array of segment points @@ -71,15 +70,6 @@ public function jsonSerialize(): array ]; } - public static function gpxSerialize(\SimpleXMLElement $node): void - { - } - - public function gpxDeserialize(\DOMDocument &$document): void - { - } - - /** * @return array|Point[] */ diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index 26de275..2f87c96 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\GpxSerializable; use phpGPX\Helpers\DateTimeHelper; use phpGPX\phpGPX; @@ -14,7 +13,7 @@ * Class Stats * @package phpGPX\Models */ -class Stats implements \JsonSerializable, GpxSerializable +class Stats implements \JsonSerializable { /** @@ -150,24 +149,4 @@ public function jsonSerialize(): array 'duration' => $this->duration, ], fn($v) => $v !== null); } - - /** - * GPX serializer - * @param \SimpleXMLElement $node - * @return void - */ - public static function gpxSerialize(\SimpleXMLElement $node): void - { - // Implementation required by GpxSerializable interface - } - - /** - * GPX deserializer - * @param \DOMDocument $document - * @return void - */ - public function gpxDeserialize(\DOMDocument &$document): void - { - // Implementation required by GpxSerializable interface - } } diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index e6186ac..c21df7f 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -13,7 +13,7 @@ * Class Track * @package phpGPX\Models */ -class Track extends Collection implements \phpGPX\GpxSerializable +class Track extends Collection { /** @@ -85,14 +85,6 @@ public function jsonSerialize(): array ]; } - public static function gpxSerialize(\SimpleXMLElement $node): void - { - } - - public function gpxDeserialize(\DOMDocument &$document): void - { - } - /** * Recalculate stats objects. * @return void From 11b09370377974b862ce412d636046c324ee1aa8 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 16:00:29 +0100 Subject: [PATCH 16/31] Add detailed roadmap for `phpGPX 2.x` outlining design principles, implementation phases, and architectural changes. --- docs/04_Development/03_Roadmap_2x.md | 335 +++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 docs/04_Development/03_Roadmap_2x.md diff --git a/docs/04_Development/03_Roadmap_2x.md b/docs/04_Development/03_Roadmap_2x.md new file mode 100644 index 0000000..25b1d63 --- /dev/null +++ b/docs/04_Development/03_Roadmap_2x.md @@ -0,0 +1,335 @@ +# Roadmap: phpGPX 2.x + +This document tracks the architectural plan and implementation phases for the phpGPX 2.0 release. +The `develop` branch is the home of all 2.x work. + +## Design Principles + +- **PHP 8.1+ only** — leverage enums, readonly properties, typed properties, union types +- **External parsers** — models stay clean; XML serialization lives in Parser classes (Data Mapper pattern) +- **Middleware pipeline** — replaces static config flags with composable, pluggable processing +- **GeoJSON-native JSON output** — `JsonSerializable` on models returns GeoJSON (RFC 7946) +- **Nullable properties** — GPX files with missing attributes are not rejected + +## Completed + +- [x] Drop PHP < 8.1 support +- [x] Upgrade to PHPUnit 10+ (supports 10.5, 11.x, 12.x) +- [x] Remove `Summarizable` interface and `toArray()` — replaced by `JsonSerializable` (#69) +- [x] Remove `GpxSerializable` interface — dead code, parsers handle XML serialization +- [x] Standardize test fixture directory naming +- [x] `PointType` enum (replaces string constants for point type mapping) + +--- + +## Phase 1: Parser Consolidation + +**Goal:** Reduce duplication across 13 parser classes by extracting shared attribute-mapping logic. + +### 1.1 — Extract `AbstractParser` base class + +All parsers (TrackParser, RouteParser, SegmentParser, PointParser, MetadataParser, LinkParser, +PersonParser, EmailParser, CopyrightParser, BoundsParser) share the same pattern: + +``` +$attributeMapper → foreach → switch (special cases) → default settype() +``` + +Extract into an abstract base: + +```php +abstract class AbstractParser +{ + abstract protected static function getAttributeMapper(): array; + abstract protected static function getTagName(): string; + + protected static function mapAttributes( + \SimpleXMLElement $node, + object $model, + array $attributeMapper + ): void { + foreach ($attributeMapper as $key => $attribute) { + if (isset($attribute['parser'])) { + // Delegate to child parser (e.g., 'link' => LinkParser::class) + continue; + } + if (!in_array($attribute['type'], ['object', 'array'])) { + if (isset($node->$key)) { + $value = (string) $node->$key; + settype($value, $attribute['type']); + $model->{$attribute['name']} = $value; + } + } + } + } + + protected static function mapToXML( + object $model, + \DOMDocument $document, + \DOMElement $node, + array $attributeMapper + ): void { + foreach ($attributeMapper as $key => $attribute) { + if (!is_null($model->{$attribute['name']})) { + $child = $document->createElement($key); + $elementText = $document->createTextNode((string) $model->{$attribute['name']}); + $child->appendChild($elementText); + $node->appendChild($child); + } + } + } +} +``` + +Each concrete parser overrides only the special-case handling (time, links, extensions, segments). + +**Files to change:** +- `src/phpGPX/Parsers/AbstractParser.php` (new) +- All 13 existing parser classes (refactor to extend AbstractParser) + +**Estimated impact:** ~40% less code in parsers, single place for type-conversion logic. + +### 1.2 — Add return type declarations to all parser methods + +Several parsers are missing return types (e.g., `TrackParser::parse()`, `TrackParser::toXML()`). +Add strict return types for PHP 8.4 compatibility (already partially done). + +### 1.3 — Unify `$attributeMapper` format + +Introduce a `'parser'` key for nested objects so special-case switches shrink: + +```php +'link' => [ + 'name' => 'links', + 'type' => 'array', + 'parser' => LinkParser::class, +], +'extensions' => [ + 'name' => 'extensions', + 'type' => 'object', + 'parser' => ExtensionParser::class, +], +'time' => [ + 'name' => 'time', + 'type' => 'datetime', // new built-in type handled by AbstractParser +], +``` + +--- + +## Phase 2: Instance-Based `phpGPX` Entry Point + +**Goal:** Replace global static configuration with an instance that carries its own settings and middleware. + +### 2.1 — Convert `phpGPX` to an instance class (#68) + +Current (static, global state): +```php +phpGPX::$CALCULATE_STATS = true; +phpGPX::$APPLY_ELEVATION_SMOOTHING = true; +$file = phpGPX::load('track.gpx'); +``` + +New (instance, injectable): +```php +$gpx = new phpGPX(); +$gpx->addMiddleware(new StatsMiddleware()); +$gpx->addMiddleware(new ElevationSmoothingMiddleware(threshold: 2, spikesThreshold: 5)); +$file = $gpx->load('track.gpx'); +``` + +Keep a static convenience `phpGPX::load()` that creates a default-configured instance for +backward-compatible simple usage. + +**Files to change:** +- `src/phpGPX/phpGPX.php` (refactor) +- `src/phpGPX/Config.php` (new — holds config as a value object instead of static properties) + +### 2.2 — Move stats calculation out of parsers + +Currently `TrackParser::parse()` calls `$track->recalculateStats()` inside the parse loop, +gated by `phpGPX::$CALCULATE_STATS`. This couples parsing to stats computation. + +Move stats calculation into a `StatsMiddleware` that runs after parsing is complete. +Parsers should only produce the model tree from XML — nothing else. + +--- + +## Phase 3: Middleware System (#68) + +**Goal:** Composable post-parse processing pipeline. + +### 3.1 — Define `MiddlewareInterface` + +```php +interface MiddlewareInterface +{ + public function process(GpxFile $gpxFile): GpxFile; +} +``` + +### 3.2 — Implement core middlewares + +| Middleware | Replaces | GitHub Issue | +|---|---|---| +| `StatsMiddleware` | `phpGPX::$CALCULATE_STATS` + `StatsCalculator` interface | — | +| `ElevationSmoothingMiddleware` | `phpGPX::$APPLY_ELEVATION_SMOOTHING` + threshold constants | #59 | +| `DistanceSmoothingMiddleware` | `phpGPX::$APPLY_DISTANCE_SMOOTHING` + threshold constant | — | +| `BoundsMiddleware` | Manual bounds — auto-compute for tracks/routes/segments | #28 | +| `TimestampSortMiddleware` | `phpGPX::$SORT_BY_TIMESTAMP` | — | +| `TrackPointExtensionStatsMiddleware` | Not yet implemented — stats from extension data (HR, cadence) | #15 | +| `MovementDurationMiddleware` | Not yet implemented — exclude pauses from duration/speed | Discussion #73 | + +### 3.3 — Default middleware stack + +The default `phpGPX()` instance ships with: +```php +[ + new StatsMiddleware(), +] +``` + +Users opt-in to everything else explicitly. This replaces the current approach where +`CALCULATE_STATS`, `APPLY_ELEVATION_SMOOTHING`, etc. are global boolean flags. + +### 3.4 — Deprecate and remove static config properties + +After middlewares are stable, remove the static properties from `phpGPX` class. +The `Config` value object replaces format-related settings (PRETTY_PRINT, DATETIME_FORMAT, etc.). + +--- + +## Phase 4: Universal Extension Processing (#41) + +**Goal:** Make it easy to add new GPX extension types without modifying core code. + +### 4.1 — `ExtensionInterface` contract + +```php +interface ExtensionInterface extends \JsonSerializable +{ + public static function getNamespace(): string; + public static function getNamespacePrefix(): string; + public static function getSchemaLocation(): string; + public static function getElementName(): string; +} +``` + +### 4.2 — Extension registry + +Replace the hardcoded `TrackPointExtension` check in `ExtensionParser` with a registry: + +```php +$gpx = new phpGPX(); +$gpx->registerExtension(TrackPointExtension::class, TrackPointExtensionParser::class); +$gpx->registerExtension(StyleExtension::class, StyleExtensionParser::class); // PR #75 +``` + +`TrackPointExtension` stays registered by default. Third-party extensions can be added +without modifying library code. + +### 4.3 — Add GPX version attribute support (#72) + +Parse and preserve the GPX `version` attribute on `GpxFile`. + +--- + +## Phase 5: Strict Typing & Model Cleanup + +**Goal:** Full typed codebase, clean model hierarchy. + +### 5.1 — Constructor promotion for simple models + +```php +// Before +class Bounds implements \JsonSerializable { + public ?float $minLatitude; + // ... + public function __construct(?float $minLatitude, ...) { + $this->minLatitude = $minLatitude; + } +} + +// After +class Bounds implements \JsonSerializable { + public function __construct( + public ?float $minLatitude = null, + public ?float $minLongitude = null, + public ?float $maxLatitude = null, + public ?float $maxLongitude = null, + ) {} +} +``` + +Apply to: Bounds, Email, Copyright, Link, Person, Extensions, TrackPointExtension. + +### 5.2 — Replace `Point` string constants with `PointType` enum usage + +`Point::WAYPOINT`, `Point::TRACKPOINT`, `Point::ROUTEPOINT` constants still exist alongside +the `PointType` enum. Remove the string constants, use the enum everywhere (parsers, tests). + +### 5.3 — `Stats` as a value object + +Consider making `Stats` immutable — constructed by `StatsMiddleware`, not mutated in-place. +The `reset()` + incremental mutation pattern is fragile. A builder or factory approach +within the middleware is cleaner. + +### 5.4 — Remove `StatsCalculator` interface from models + +Once stats computation moves to `StatsMiddleware`, models no longer need `recalculateStats()` +or `getPoints()` on the `StatsCalculator` interface. `getPoints()` can stay as a convenience +method on `Collection` without being interface-mandated. + +### 5.5 — Fix startedAt/finishedAt for missing timestamps (#51) + +Ensure the stats middleware correctly scans for the first/last non-null timestamp +rather than assuming boundary points have timestamps. (Partially addressed in current code, +verify with dedicated test cases.) + +--- + +## Phase 6: Documentation & Release + +### 6.1 — Fix documentation generation (#76) + +Evaluate phpDocumentor vs alternatives. Set up automated doc builds in CI. + +### 6.2 — Migration guide (1.x → 2.x) + +Document all breaking changes: +- Removed `Summarizable` / `toArray()` — use `jsonSerialize()` +- Removed `GpxSerializable` +- Instance-based API vs static methods +- Middleware system vs static config flags +- Extension registry vs hardcoded extensions +- `PointType` enum vs string constants + +### 6.3 — Update all existing docs + +Rewrite Getting Started, Usage, Configuration, Extensions sections to reflect 2.x API. + +### 6.4 — Performance benchmarks (1.x vs 2.x) + +Benchmark parsing and serialization of large GPX files. Ensure no regressions +from the architectural changes. + +### 6.5 — Tag `2.0.0-beta.1`, then `2.0.0` + +--- + +## Issue Tracker Cross-Reference + +| Issue | Title | Phase | +|---|---|---| +| #15 | Create statistics from GPX extensions | Phase 3 (TrackPointExtensionStatsMiddleware) | +| #28 | Statistics - get Bounds of GPX Routes | Phase 3 (BoundsMiddleware) | +| #41 | Implementing waypoint and creation time extensions | Phase 4 (Extension registry) | +| #51 | startedAt/finishedAt missing timestamps | Phase 5.5 | +| #59 | Elevation gain/loss accuracy | Phase 3 (ElevationSmoothingMiddleware) | +| #68 | Middlewares | Phase 2 + 3 | +| #69 | Removal of Summarizable and toArray | Completed | +| #70 | Min altitude not necessarily first point | Completed (verify) | +| #72 | Add GPX version attribute | Phase 4.3 | +| #73 | Movement duration statistics (discussion) | Phase 3 (MovementDurationMiddleware) | +| #75 | Style extension (draft PR) | Phase 4 (Extension registry) | +| #76 | Fix documentation generation | Phase 6.1 | \ No newline at end of file From 79c61d2f4f808245051a0b4827c75c53e625e133 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 16:40:12 +0100 Subject: [PATCH 17/31] =?UTF-8?q?Refactor=20parsers=20to=20utilize=20`Abst?= =?UTF-8?q?ractParser`=20for=20centralized=20attribute=20mapping=20and=20X?= =?UTF-8?q?ML=20handling=20(I=20guess=20good=20idea=20=F0=9F=98=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/04_Development/03_Roadmap_2x.md | 121 ++------- src/phpGPX/Parsers/AbstractParser.php | 151 +++++++++++ src/phpGPX/Parsers/BoundsParser.php | 2 +- src/phpGPX/Parsers/CopyrightParser.php | 6 +- src/phpGPX/Parsers/EmailParser.php | 2 +- src/phpGPX/Parsers/ExtensionParser.php | 8 +- .../Extensions/TrackPointExtensionParser.php | 94 ++++--- src/phpGPX/Parsers/LinkParser.php | 50 +--- src/phpGPX/Parsers/MetadataParser.php | 184 ++++++------- src/phpGPX/Parsers/PersonParser.php | 20 +- src/phpGPX/Parsers/PointParser.php | 251 ++++++++---------- src/phpGPX/Parsers/RouteParser.php | 171 +++++------- src/phpGPX/Parsers/SegmentParser.php | 62 ++--- src/phpGPX/Parsers/TrackParser.php | 163 ++++-------- src/phpGPX/Parsers/WaypointParser.php | 2 +- tests/Unit/Parsers/LinkParserTest.php | 13 +- 16 files changed, 585 insertions(+), 715 deletions(-) create mode 100644 src/phpGPX/Parsers/AbstractParser.php diff --git a/docs/04_Development/03_Roadmap_2x.md b/docs/04_Development/03_Roadmap_2x.md index 25b1d63..f3de36d 100644 --- a/docs/04_Development/03_Roadmap_2x.md +++ b/docs/04_Development/03_Roadmap_2x.md @@ -11,8 +11,12 @@ The `develop` branch is the home of all 2.x work. - **GeoJSON-native JSON output** — `JsonSerializable` on models returns GeoJSON (RFC 7946) - **Nullable properties** — GPX files with missing attributes are not rejected +--- + ## Completed +### Pre-Phase + - [x] Drop PHP < 8.1 support - [x] Upgrade to PHPUnit 10+ (supports 10.5, 11.x, 12.x) - [x] Remove `Summarizable` interface and `toArray()` — replaced by `JsonSerializable` (#69) @@ -20,100 +24,29 @@ The `develop` branch is the home of all 2.x work. - [x] Standardize test fixture directory naming - [x] `PointType` enum (replaces string constants for point type mapping) ---- - -## Phase 1: Parser Consolidation - -**Goal:** Reduce duplication across 13 parser classes by extracting shared attribute-mapping logic. - -### 1.1 — Extract `AbstractParser` base class - -All parsers (TrackParser, RouteParser, SegmentParser, PointParser, MetadataParser, LinkParser, -PersonParser, EmailParser, CopyrightParser, BoundsParser) share the same pattern: - -``` -$attributeMapper → foreach → switch (special cases) → default settype() -``` - -Extract into an abstract base: - -```php -abstract class AbstractParser -{ - abstract protected static function getAttributeMapper(): array; - abstract protected static function getTagName(): string; - - protected static function mapAttributes( - \SimpleXMLElement $node, - object $model, - array $attributeMapper - ): void { - foreach ($attributeMapper as $key => $attribute) { - if (isset($attribute['parser'])) { - // Delegate to child parser (e.g., 'link' => LinkParser::class) - continue; - } - if (!in_array($attribute['type'], ['object', 'array'])) { - if (isset($node->$key)) { - $value = (string) $node->$key; - settype($value, $attribute['type']); - $model->{$attribute['name']} = $value; - } - } - } - } - - protected static function mapToXML( - object $model, - \DOMDocument $document, - \DOMElement $node, - array $attributeMapper - ): void { - foreach ($attributeMapper as $key => $attribute) { - if (!is_null($model->{$attribute['name']})) { - $child = $document->createElement($key); - $elementText = $document->createTextNode((string) $model->{$attribute['name']}); - $child->appendChild($elementText); - $node->appendChild($child); - } - } - } -} -``` - -Each concrete parser overrides only the special-case handling (time, links, extensions, segments). - -**Files to change:** -- `src/phpGPX/Parsers/AbstractParser.php` (new) -- All 13 existing parser classes (refactor to extend AbstractParser) - -**Estimated impact:** ~40% less code in parsers, single place for type-conversion logic. - -### 1.2 — Add return type declarations to all parser methods - -Several parsers are missing return types (e.g., `TrackParser::parse()`, `TrackParser::toXML()`). -Add strict return types for PHP 8.4 compatibility (already partially done). - -### 1.3 — Unify `$attributeMapper` format - -Introduce a `'parser'` key for nested objects so special-case switches shrink: - -```php -'link' => [ - 'name' => 'links', - 'type' => 'array', - 'parser' => LinkParser::class, -], -'extensions' => [ - 'name' => 'extensions', - 'type' => 'object', - 'parser' => ExtensionParser::class, -], -'time' => [ - 'name' => 'time', - 'type' => 'datetime', // new built-in type handled by AbstractParser -], -``` +### Phase 1: Parser Consolidation + +- [x] **1.1 — AbstractParser base class** + Extracted `AbstractParser` with four methods: `mapAttributesFromXML()`, `mapAttributesToXML()`, + `parseDelegated()`, `serializeDelegated()`. Five parsers refactored to extend it: + TrackParser, RouteParser, PointParser, MetadataParser, TrackPointExtensionParser. + Remaining 8 parsers (SegmentParser, LinkParser, PersonParser, EmailParser, CopyrightParser, + BoundsParser, ExtensionParser, WaypointParser) are standalone — they are too small or too + specialized to benefit from the base class. + +- [x] **1.2 — Return type declarations on all parser methods** + All `parse()` and `toXML()` methods across all 13 parsers now have explicit return types. + All `$tagName` properties typed as `string`. + +- [x] **1.3 — Unified `$attributeMapper` format** + Attribute mappers in refactored parsers use a `'parser'` key for delegated parsing and + `'datetime'` type for DateTime fields. All switch/case blocks for delegation eliminated. + Unified parser contract: every `parse()` accepts a single `SimpleXMLElement` node and + returns a single model object (or null). Collection iteration is handled by + `AbstractParser::parseDelegated()` based on `'type' => 'array'`. + SegmentParser and LinkParser standardized to single-node contract. + Removed all `toXMLArray` methods from refactored parsers — iteration handled by + `serializeDelegated()`. --- diff --git a/src/phpGPX/Parsers/AbstractParser.php b/src/phpGPX/Parsers/AbstractParser.php new file mode 100644 index 0000000..e6351af --- /dev/null +++ b/src/phpGPX/Parsers/AbstractParser.php @@ -0,0 +1,151 @@ + [ + * 'name' => 'modelPropertyName', + * 'type' => 'string|int|integer|float|object|array|datetime', + * 'parser' => ParserClass::class, // optional, for delegated parsing + * 'xmlAttribute' => true, // optional, read from XML attribute instead of child element + * ] + * + * Parser contract: every parser's parse() accepts a single SimpleXMLElement node + * and returns a single model object (or null). Iteration over collections is handled + * here in parseDelegated/serializeDelegated based on the 'type' key. + * + * @package phpGPX\Parsers + */ +abstract class AbstractParser +{ + /** + * @return array + */ + abstract protected static function getAttributeMapper(): array; + + /** + * Map scalar attributes from an XML node onto a model object. + * Skips entries with 'parser', 'datetime', 'object', or 'array' types. + * + * @param \SimpleXMLElement $node + * @param object $model + */ + protected static function mapAttributesFromXML(\SimpleXMLElement $node, object $model): void + { + foreach (static::getAttributeMapper() as $xmlKey => $attribute) { + if (isset($attribute['parser']) || in_array($attribute['type'], ['object', 'array', 'datetime'])) { + continue; + } + + if (!empty($attribute['xmlAttribute'])) { + if (isset($node[$xmlKey])) { + $value = (string) $node[$xmlKey]; + settype($value, $attribute['type']); + $model->{$attribute['name']} = $value; + } + } else { + if (isset($node->$xmlKey)) { + $value = (string) $node->$xmlKey; + settype($value, $attribute['type']); + $model->{$attribute['name']} = $value; + } + } + } + } + + /** + * Map scalar model properties onto XML elements. + * Skips entries with 'parser', 'datetime', 'object', or 'array' types. + * + * @param object $model + * @param \DOMDocument $document + * @param \DOMElement $node + */ + protected static function mapAttributesToXML(object $model, \DOMDocument &$document, \DOMElement $node): void + { + foreach (static::getAttributeMapper() as $xmlKey => $attribute) { + $value = $model->{$attribute['name']} ?? null; + if ($value === null) { + continue; + } + + if (isset($attribute['parser']) || in_array($attribute['type'], ['object', 'array', 'datetime'])) { + continue; + } + + if (!empty($attribute['xmlAttribute'])) { + $node->setAttribute($xmlKey, (string) $value); + } else { + $child = $document->createElement($xmlKey); + $child->appendChild($document->createTextNode((string) $value)); + $node->appendChild($child); + } + } + } + + /** + * Parse a delegated child using the parser class from the attribute mapper. + * + * For 'object' types: calls parser::parse() once, returns the model or null. + * For 'array' types: iterates child elements, calls parser::parse() per element, + * collects non-null results into an array. + * + * @param \SimpleXMLElement $parentNode + * @param string $xmlKey + * @param array $attribute + * @return mixed + */ + protected static function parseDelegated(\SimpleXMLElement $parentNode, string $xmlKey, array $attribute): mixed + { + if (!isset($parentNode->$xmlKey)) { + return $attribute['type'] === 'array' ? [] : null; + } + + $parserClass = $attribute['parser']; + + if ($attribute['type'] === 'array') { + $items = []; + foreach ($parentNode->$xmlKey as $childNode) { + $item = $parserClass::parse($childNode); + if ($item !== null) { + $items[] = $item; + } + } + return $items; + } + + return $parserClass::parse($parentNode->$xmlKey); + } + + /** + * Serialize a delegated property to XML using the parser class from the attribute mapper. + * + * For 'object' types: calls parser::toXML() once, appends the element. + * For 'array' types: iterates items, calls parser::toXML() per item, appends each. + * + * @param mixed $value + * @param array $attribute + * @param \DOMDocument $document + * @param \DOMElement $parentNode + */ + protected static function serializeDelegated(mixed $value, array $attribute, \DOMDocument &$document, \DOMElement $parentNode): void + { + if ($value === null || (is_array($value) && empty($value))) { + return; + } + + $parserClass = $attribute['parser']; + + if ($attribute['type'] === 'array') { + foreach ($value as $item) { + $parentNode->appendChild($parserClass::toXML($item, $document)); + } + } else { + $parentNode->appendChild($parserClass::toXML($value, $document)); + } + } +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/BoundsParser.php b/src/phpGPX/Parsers/BoundsParser.php index 21c7f2e..a961e6b 100644 --- a/src/phpGPX/Parsers/BoundsParser.php +++ b/src/phpGPX/Parsers/BoundsParser.php @@ -14,7 +14,7 @@ */ abstract class BoundsParser { - private static $tagName = 'bounds'; + private static string $tagName = 'bounds'; /** * Parse data from XML. diff --git a/src/phpGPX/Parsers/CopyrightParser.php b/src/phpGPX/Parsers/CopyrightParser.php index d5d97d7..0d5a08e 100644 --- a/src/phpGPX/Parsers/CopyrightParser.php +++ b/src/phpGPX/Parsers/CopyrightParser.php @@ -14,13 +14,13 @@ */ abstract class CopyrightParser { - public static $tagName = 'copyright'; + public static string $tagName = 'copyright'; /** * @param \SimpleXMLElement $node * @return Copyright|null */ - public static function parse(\SimpleXMLElement $node) + public static function parse(\SimpleXMLElement $node): ?Copyright { if ($node->getName() != self::$tagName) { return null; @@ -40,7 +40,7 @@ public static function parse(\SimpleXMLElement $node) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Copyright $copyright, \DOMDocument &$document) + public static function toXML(Copyright $copyright, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); diff --git a/src/phpGPX/Parsers/EmailParser.php b/src/phpGPX/Parsers/EmailParser.php index ab0812f..adc74f2 100644 --- a/src/phpGPX/Parsers/EmailParser.php +++ b/src/phpGPX/Parsers/EmailParser.php @@ -14,7 +14,7 @@ */ abstract class EmailParser { - private static $tagName = 'email'; + private static string $tagName = 'email'; /** * @param \SimpleXMLElement $node diff --git a/src/phpGPX/Parsers/ExtensionParser.php b/src/phpGPX/Parsers/ExtensionParser.php index 4b6154b..07f0814 100644 --- a/src/phpGPX/Parsers/ExtensionParser.php +++ b/src/phpGPX/Parsers/ExtensionParser.php @@ -16,15 +16,15 @@ */ abstract class ExtensionParser { - public static $tagName = 'extensions'; + public static string $tagName = 'extensions'; - public static $usedNamespaces = []; + public static array $usedNamespaces = []; /** * @param \SimpleXMLElement $nodes * @return Extensions */ - public static function parse($nodes) + public static function parse(\SimpleXMLElement $nodes): Extensions { $extensions = new Extensions(); @@ -55,7 +55,7 @@ public static function parse($nodes) * @param \DOMDocument $document * @return \DOMElement|null */ - public static function toXML(Extensions $extensions, \DOMDocument &$document) + public static function toXML(Extensions $extensions, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); diff --git a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php index 46dd028..0e789af 100644 --- a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php +++ b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php @@ -7,62 +7,58 @@ namespace phpGPX\Parsers\Extensions; use phpGPX\Models\Extensions\TrackPointExtension; +use phpGPX\Parsers\AbstractParser; use phpGPX\Parsers\ExtensionParser; -class TrackPointExtensionParser +class TrackPointExtensionParser extends AbstractParser { - private static $attributeMapper = [ - 'atemp' => [ - 'name' => 'aTemp', - 'type' => 'float' - ], - 'wtemp' => [ - 'name' => 'wTemp', - 'type' => 'float' - ], - 'depth' => [ - 'name' => 'depth', - 'type' => 'float' - ], - 'hr' => [ - 'name' => 'hr', - 'type' => 'float' - ], - 'cad' => [ - 'name' => 'cad', - 'type' => 'float' - ], - 'speed' => [ - 'name' => 'speed', - 'type' => 'float' - ], - 'course' => [ - 'name' => 'course', - 'type' => 'int' - ], - 'bearing' => [ - 'name' => 'bearing', - 'type' => 'int' - ] - ]; + protected static function getAttributeMapper(): array + { + return [ + 'atemp' => [ + 'name' => 'aTemp', + 'type' => 'float' + ], + 'wtemp' => [ + 'name' => 'wTemp', + 'type' => 'float' + ], + 'depth' => [ + 'name' => 'depth', + 'type' => 'float' + ], + 'hr' => [ + 'name' => 'hr', + 'type' => 'float' + ], + 'cad' => [ + 'name' => 'cad', + 'type' => 'float' + ], + 'speed' => [ + 'name' => 'speed', + 'type' => 'float' + ], + 'course' => [ + 'name' => 'course', + 'type' => 'int' + ], + 'bearing' => [ + 'name' => 'bearing', + 'type' => 'int' + ] + ]; + } /** * @param \SimpleXMLElement $node * @return TrackPointExtension */ - public static function parse($node) + public static function parse(\SimpleXMLElement $node): TrackPointExtension { $extension = new TrackPointExtension(); - foreach (self::$attributeMapper as $key => $attribute) { - $value = isset($node->$key) ? $node->$key : null; - - if (!is_null($value)) { - settype($value, $attribute['type']); - } - - $extension->{$attribute['name']} = $value; - } + self::mapAttributesFromXML($node, $extension); return $extension; } @@ -72,9 +68,9 @@ public static function parse($node) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(TrackPointExtension $extension, \DOMDocument &$document) + public static function toXML(TrackPointExtension $extension, \DOMDocument &$document): \DOMElement { - $node = $document->createElement("gpxtpx:TrackPointExtension"); + $node = $document->createElement("gpxtpx:TrackPointExtension"); ExtensionParser::$usedNamespaces[TrackPointExtension::EXTENSION_NAME] = [ 'namespace' => TrackPointExtension::EXTENSION_NAMESPACE, @@ -83,7 +79,7 @@ public static function toXML(TrackPointExtension $extension, \DOMDocument &$docu 'prefix' => TrackPointExtension::EXTENSION_NAMESPACE_PREFIX ]; - foreach (self::$attributeMapper as $key => $attribute) { + foreach (self::getAttributeMapper() as $key => $attribute) { if (isset($extension->{$attribute['name']})) { $child = $document->createElement( sprintf("%s:%s", TrackPointExtension::EXTENSION_NAMESPACE_PREFIX, $key), @@ -95,4 +91,4 @@ public static function toXML(TrackPointExtension $extension, \DOMDocument &$docu return $node; } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/LinkParser.php b/src/phpGPX/Parsers/LinkParser.php index 30042fc..7e8d7c7 100644 --- a/src/phpGPX/Parsers/LinkParser.php +++ b/src/phpGPX/Parsers/LinkParser.php @@ -10,46 +10,22 @@ abstract class LinkParser { - private static $tagName = 'link'; + private static string $tagName = 'link'; /** - * @param \SimpleXMLElement|\SimpleXMLElement[] $nodes - * @return Link[] + * Parse a single link node. + * + * @param \SimpleXMLElement $node + * @return Link */ - public static function parse($nodes): array + public static function parse(\SimpleXMLElement $node): Link { - $links = []; + $link = new Link(); + $link->href = isset($node['href']) ? (string) $node['href'] : null; + $link->text = isset($node->text) ? (string) $node->text : null; + $link->type = isset($node->type) ? (string) $node->type : null; - // Handle both a single SimpleXMLElement and an array of SimpleXMLElements - if (!is_array($nodes)) { - $nodes = [$nodes]; - } - - foreach ($nodes as $node) { - $link = new Link(); - $link->href = isset($node['href']) ? (string) $node['href'] : null; - $link->text = isset($node->text) ? (string) $node->text : null; - $link->type = isset($node->type) ? (string) $node->type : null; - - $links[] = $link; - } - return $links; - } - - /** - * @param Link[] $links - * @param \DOMDocument $document - * @return \DOMElement[] - */ - public static function toXMLArray(array $links, \DOMDocument &$document): array - { - $result = []; - - foreach ($links as $link) { - $result[] = self::toXML($link, $document); - } - - return $result; + return $link; } /** @@ -59,7 +35,7 @@ public static function toXMLArray(array $links, \DOMDocument &$document): array */ public static function toXML(Link $link, \DOMDocument &$document): \DOMElement { - $node = $document->createElement(self::$tagName); + $node = $document->createElement(self::$tagName); if ($link->href !== null && $link->href !== '') { $node->setAttribute('href', $link->href); @@ -77,4 +53,4 @@ public static function toXML(Link $link, \DOMDocument &$document): \DOMElement return $node; } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/MetadataParser.php b/src/phpGPX/Parsers/MetadataParser.php index 885a245..fa00311 100644 --- a/src/phpGPX/Parsers/MetadataParser.php +++ b/src/phpGPX/Parsers/MetadataParser.php @@ -13,134 +13,104 @@ * Class MetadataParser * @package phpGPX\Parsers */ -abstract class MetadataParser +abstract class MetadataParser extends AbstractParser { - private static $tagName = 'metadata'; + private static string $tagName = 'metadata'; - private static $attributeMapper = [ - 'name' => [ - 'name' => 'name', - 'type' => 'string' - ], - 'desc' => [ - 'name' => 'description', - 'type' => 'string' - ], - 'author' => [ - 'name' => 'author', - 'type' => 'object' - ], - 'copyright' => [ - 'name' => 'copyright', - 'type' => 'object' - ], - 'link' => [ - 'name' => 'links', - 'type' => 'array' - ], - 'time' => [ - 'name' => 'time', - 'type' => 'object' - ], - 'keywords' => [ - 'name' => 'keywords', - 'type' => 'string' - ], - 'bounds' => [ - 'name' => 'bounds', - 'type' => 'object' - ], - 'extensions' => [ - 'name' => 'extensions', - 'type' => 'object' - ] - ]; + protected static function getAttributeMapper(): array + { + return [ + 'name' => [ + 'name' => 'name', + 'type' => 'string' + ], + 'desc' => [ + 'name' => 'description', + 'type' => 'string' + ], + 'author' => [ + 'name' => 'author', + 'type' => 'object', + 'parser' => PersonParser::class, + ], + 'copyright' => [ + 'name' => 'copyright', + 'type' => 'object', + 'parser' => CopyrightParser::class, + ], + 'link' => [ + 'name' => 'links', + 'type' => 'array', + 'parser' => LinkParser::class, + ], + 'time' => [ + 'name' => 'time', + 'type' => 'datetime' + ], + 'keywords' => [ + 'name' => 'keywords', + 'type' => 'string' + ], + 'bounds' => [ + 'name' => 'bounds', + 'type' => 'object', + 'parser' => BoundsParser::class, + ], + 'extensions' => [ + 'name' => 'extensions', + 'type' => 'object', + 'parser' => ExtensionParser::class, + ] + ]; + } /** * @param \SimpleXMLElement $node * @return Metadata */ - public static function parse(\SimpleXMLElement $node) + public static function parse(\SimpleXMLElement $node): Metadata { $metadata = new Metadata(); - foreach (self::$attributeMapper as $key => $attribute) { - switch ($key) { - case 'author': - $metadata->author = isset($node->author) ? PersonParser::parse($node->author) : null; - break; - case 'copyright': - $metadata->copyright = isset($node->copyright) ? CopyrightParser::parse($node->copyright) : null; - break; - case 'link': - $metadata->links = isset($node->link) ? LinkParser::parse($node->link) : null; - break; - case 'time': - $metadata->time = isset($node->time) ? DateTimeHelper::parseDateTime($node->time) : null; - break; - case 'bounds': - $metadata->bounds = isset($node->bounds) ? BoundsParser::parse($node->bounds) : null; - break; - case 'extensions': - $metadata->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; - break; - default: - if (!in_array($attribute['type'], ['object', 'array'])) { - if (isset($node->$key)) { - $value = (string) $node->$key; - settype($value, $attribute['type']); - $metadata->{$attribute['name']} = $value; - } - } - break; + self::mapAttributesFromXML($node, $metadata); + + // Datetime + $metadata->time = isset($node->time) ? DateTimeHelper::parseDateTime($node->time) : null; + + // Delegated parsers + foreach (self::getAttributeMapper() as $key => $attribute) { + if (isset($attribute['parser'])) { + $metadata->{$attribute['name']} = self::parseDelegated($node, $key, $attribute); } } return $metadata; } - public static function toXML(Metadata $metadata, \DOMDocument &$document) + /** + * @param Metadata $metadata + * @param \DOMDocument $document + * @return \DOMElement + */ + public static function toXML(Metadata $metadata, \DOMDocument &$document): \DOMElement { - $node = $document->createElement(self::$tagName); + $node = $document->createElement(self::$tagName); - foreach (self::$attributeMapper as $key => $attribute) { - if (!is_null($metadata->{$attribute['name']})) { - switch ($key) { - case 'author': - $child = PersonParser::toXML($metadata->author, $document); - break; - case 'copyright': - $child = CopyrightParser::toXML($metadata->copyright, $document); - break; - case 'link': - $child = LinkParser::toXMLArray($metadata->links, $document); - break; - case 'time': - $child = $document->createElement('time', DateTimeHelper::formatDateTime($metadata->time)); - break; - case 'bounds': - $child = BoundsParser::toXML($metadata->bounds, $document); - break; - case 'extensions': - $child = ExtensionParser::toXML($metadata->extensions, $document); - break; - default: - $child = $document->createElement($key); - $elementText = $document->createTextNode((string) $metadata->{$attribute['name']}); - $child->appendChild($elementText); - break; - } + self::mapAttributesToXML($metadata, $document, $node); + + // Datetime + if ($metadata->time !== null) { + $child = $document->createElement('time', DateTimeHelper::formatDateTime($metadata->time)); + $node->appendChild($child); + } - if (is_array($child)) { - foreach ($child as $item) { - $node->appendChild($item); - } - } else { - $node->appendChild($child); - } + // Delegated parsers + foreach (self::getAttributeMapper() as $key => $attribute) { + if (isset($attribute['parser'])) { + self::serializeDelegated($metadata->{$attribute['name']}, $attribute, $document, $node); } } return $node; } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/PersonParser.php b/src/phpGPX/Parsers/PersonParser.php index b4b682f..fc9afb5 100644 --- a/src/phpGPX/Parsers/PersonParser.php +++ b/src/phpGPX/Parsers/PersonParser.php @@ -14,24 +14,30 @@ */ abstract class PersonParser { - public static $tagName = 'author'; + public static string $tagName = 'author'; /** * @param \SimpleXMLElement $node * @return Person */ - public static function parse(\SimpleXMLElement $node) + public static function parse(\SimpleXMLElement $node): Person { $person = new Person(); $person->name = isset($node->name) ? ((string) $node->name) : null; $person->email = isset($node->email) ? EmailParser::parse($node->email) : null; - $person->links = isset($node->link) ? LinkParser::parse($node->link) : null; + $person->links = null; + if (isset($node->link)) { + $person->links = []; + foreach ($node->link as $linkNode) { + $person->links[] = LinkParser::parse($linkNode); + } + } return $person; } - public static function toXML(Person $person, \DOMDocument &$document) + public static function toXML(Person $person, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); @@ -45,11 +51,9 @@ public static function toXML(Person $person, \DOMDocument &$document) $node->appendChild($child); } - # TODO: is_iterable - if (!is_null($person->links)) { + if (!empty($person->links)) { foreach ($person->links as $link) { - $child = LinkParser::toXML($link, $document); - $node->appendChild($child); + $node->appendChild(LinkParser::toXML($link, $document)); } } diff --git a/src/phpGPX/Parsers/PointParser.php b/src/phpGPX/Parsers/PointParser.php index c81dede..87b8d9c 100644 --- a/src/phpGPX/Parsers/PointParser.php +++ b/src/phpGPX/Parsers/PointParser.php @@ -9,88 +9,93 @@ use phpGPX\Helpers\DateTimeHelper; use phpGPX\Models\Point; -abstract class PointParser +abstract class PointParser extends AbstractParser { - private static $attributeMapper = [ - 'ele' => [ - 'name' => 'elevation', - 'type' => 'float' - ], - 'time' => [ - 'name' => 'time', - 'type' => 'object' - ], - 'magvar' => [ - 'name' => 'magVar', - 'type' => 'float' - ], - 'geoidheight' => [ - 'name' => 'geoidHeight', - 'type' => 'float' - ], - 'name' => [ - 'name' => 'name', - 'type' => 'string' - ], - 'cmt' => [ - 'name' => 'comment', - 'type' => 'string' - ], - 'desc' => [ - 'name' => 'description', - 'type' => 'string' - ], - 'src' => [ - 'name' => 'source', - 'type' => 'string' - ], - 'link' => [ - 'name' => 'links', - 'type' => 'object' - ], - 'sym' => [ - 'name' => 'symbol', - 'type' => 'string' - ], - 'type' => [ - 'name' => 'type', - 'type' => 'string' - ], - 'fix' => [ - 'name' => 'fix', - 'type' => 'string' - ], - 'sat' => [ - 'name' => 'satellitesNumber', - 'type' => 'integer' - ], - 'hdop' => [ - 'name' => 'hdop', - 'type' => 'float' - ], - 'vdop' => [ - 'name' => 'vdop', - 'type' => 'float' - ], - 'pdop' => [ - 'name' => 'pdop', - 'type' => 'float' - ], - 'ageofdgpsdata' => [ - 'name' => 'ageOfGpsData', - 'type' => 'float' - ], - 'dgpsid' => [ - 'name' => 'dgpsid', - 'type' => 'integer' - ], - 'extensions' => [ - 'name' => 'extensions', - 'type' => 'object' - ] - ]; + protected static function getAttributeMapper(): array + { + return [ + 'ele' => [ + 'name' => 'elevation', + 'type' => 'float' + ], + 'time' => [ + 'name' => 'time', + 'type' => 'datetime' + ], + 'magvar' => [ + 'name' => 'magVar', + 'type' => 'float' + ], + 'geoidheight' => [ + 'name' => 'geoidHeight', + 'type' => 'float' + ], + 'name' => [ + 'name' => 'name', + 'type' => 'string' + ], + 'cmt' => [ + 'name' => 'comment', + 'type' => 'string' + ], + 'desc' => [ + 'name' => 'description', + 'type' => 'string' + ], + 'src' => [ + 'name' => 'source', + 'type' => 'string' + ], + 'link' => [ + 'name' => 'links', + 'type' => 'array', + 'parser' => LinkParser::class, + ], + 'sym' => [ + 'name' => 'symbol', + 'type' => 'string' + ], + 'type' => [ + 'name' => 'type', + 'type' => 'string' + ], + 'fix' => [ + 'name' => 'fix', + 'type' => 'string' + ], + 'sat' => [ + 'name' => 'satellitesNumber', + 'type' => 'integer' + ], + 'hdop' => [ + 'name' => 'hdop', + 'type' => 'float' + ], + 'vdop' => [ + 'name' => 'vdop', + 'type' => 'float' + ], + 'pdop' => [ + 'name' => 'pdop', + 'type' => 'float' + ], + 'ageofdgpsdata' => [ + 'name' => 'ageOfGpsData', + 'type' => 'float' + ], + 'dgpsid' => [ + 'name' => 'dgpsid', + 'type' => 'integer' + ], + 'extensions' => [ + 'name' => 'extensions', + 'type' => 'object', + 'parser' => ExtensionParser::class, + ] + ]; + } - private static $typeMapper = [ + private static array $typeMapper = [ 'trkpt' => Point::TRACKPOINT, 'wpt' => Point::WAYPOINT, 'rtept' => Point::ROUTEPOINT @@ -107,28 +112,15 @@ public static function parse(\SimpleXMLElement $node): ?Point $point->latitude = isset($node['lat']) ? ((float) $node['lat']) : null; $point->longitude = isset($node['lon']) ? ((float) $node['lon']) : null; - foreach (self::$attributeMapper as $key => $attribute) { - switch ($key) { - case 'time': - $point->time = isset($node->time) ? DateTimeHelper::parseDateTime($node->time) : null; - break; - case 'extensions': - $point->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; - break; - case 'link': - $point->links = isset($node->link) ? LinkParser::parse($node->link) : []; - break; - default: - if (!in_array($attribute['type'], ['object', 'array'])) { - if (isset($node->$key)) { - $value = (string) $node->$key; - settype($value, $attribute['type']); - $point->{$attribute['name']} = $value; - } - } - break; - } - } + self::mapAttributesFromXML($node, $point); + + // Datetime + $point->time = isset($node->time) ? DateTimeHelper::parseDateTime($node->time) : null; + + // Delegated parsers + $mapper = self::getAttributeMapper(); + $point->links = self::parseDelegated($node, 'link', $mapper['link']); + $point->extensions = self::parseDelegated($node, 'extensions', $mapper['extensions']); return $point; } @@ -149,51 +141,20 @@ public static function toXML(Point $point, \DOMDocument &$document): \DOMElement $node->setAttribute('lon', $point->longitude); } - foreach (self::$attributeMapper as $key => $attribute) { - if (!is_null($point->{$attribute['name']})) { - switch ($key) { - case 'link': - $child = LinkParser::toXMLArray($point->links, $document); - break; - case 'time': - $child = $document->createElement('time', DateTimeHelper::formatDateTime($point->time)); - break; - case 'extensions': - $child = ExtensionParser::toXML($point->extensions, $document); - break; - default: - $child = $document->createElement($key); - $elementText = $document->createTextNode((string) $point->{$attribute['name']}); - $child->appendChild($elementText); - break; - } - - if (is_array($child)) { - foreach ($child as $item) { - $node->appendChild($item); - } - } else { - $node->appendChild($child); - } - } + self::mapAttributesToXML($point, $document, $node); + + // Datetime + if ($point->time !== null) { + $child = $document->createElement('time', DateTimeHelper::formatDateTime($point->time)); + $node->appendChild($child); } + // Delegated parsers + $mapper = self::getAttributeMapper(); + self::serializeDelegated($point->links, $mapper['link'], $document, $node); + self::serializeDelegated($point->extensions, $mapper['extensions'], $document, $node); + return $node; } - /** - * @param array $points - * @param \DOMDocument $document - * @return \DOMElement[] - */ - public static function toXMLArray(array $points, \DOMDocument &$document): array - { - $result = []; - - foreach ($points as $point) { - $result[] = self::toXML($point, $document); - } - - return $result; - } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/RouteParser.php b/src/phpGPX/Parsers/RouteParser.php index aa8982c..45349a0 100644 --- a/src/phpGPX/Parsers/RouteParser.php +++ b/src/phpGPX/Parsers/RouteParser.php @@ -13,86 +13,71 @@ * Class RouteParser * @package phpGPX\Parsers */ -abstract class RouteParser +abstract class RouteParser extends AbstractParser { - public static $tagName = 'rte'; - - private static $attributeMapper = [ - 'name' => [ - 'name' => 'name', - 'type' => 'string' - ], - 'cmt' => [ - 'name' => 'comment', - 'type' => 'string' - ], - 'desc' => [ - 'name' => 'description', - 'type' => 'string' - ], - 'src' => [ - 'name' => 'source', - 'type' => 'string' - ], - 'links' => [ - 'name' => 'links', - 'type' => 'array' - ], - 'number' => [ - 'name' => 'number', - 'type' => 'integer' - ], - 'type' => [ - 'name' => 'type', - 'type' => 'string' - ], - 'extensions' => [ - 'name' => 'extensions', - 'type' => 'object' - ], - 'rtept' => [ - 'name' => 'points', - 'type' => 'array' - ], - ]; + public static string $tagName = 'rte'; + + protected static function getAttributeMapper(): array + { + return [ + 'name' => [ + 'name' => 'name', + 'type' => 'string' + ], + 'cmt' => [ + 'name' => 'comment', + 'type' => 'string' + ], + 'desc' => [ + 'name' => 'description', + 'type' => 'string' + ], + 'src' => [ + 'name' => 'source', + 'type' => 'string' + ], + 'link' => [ + 'name' => 'links', + 'type' => 'array', + 'parser' => LinkParser::class, + ], + 'number' => [ + 'name' => 'number', + 'type' => 'integer' + ], + 'type' => [ + 'name' => 'type', + 'type' => 'string' + ], + 'extensions' => [ + 'name' => 'extensions', + 'type' => 'object', + 'parser' => ExtensionParser::class, + ], + 'rtept' => [ + 'name' => 'points', + 'type' => 'array', + 'parser' => PointParser::class, + ], + ]; + } /** - * @param \SimpleXMLElement[] $nodes + * @param \SimpleXMLElement $nodes * @return Route[] */ - public static function parse($nodes) + public static function parse(\SimpleXMLElement $nodes): array { $routes = []; foreach ($nodes as $node) { $route = new Route(); - foreach (self::$attributeMapper as $key => $attribute) { - switch ($key) { - case 'link': - $route->links = isset($node->link) ? LinkParser::parse($node->link) : []; - break; - case 'extensions': - $route->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; - break; - case 'rtept': - $route->points = []; - - if (isset($node->rtept)) { - foreach ($node->rtept as $point) { - $route->points[] = PointParser::parse($point); - } - } - break; - default: - if (!in_array($attribute['type'], ['object', 'array'])) { - if (isset($node->$key)) { - $value = (string) $node->$key; - settype($value, $attribute['type']); - $route->{$attribute['name']} = $value; - } - } - break; + self::mapAttributesFromXML($node, $route); + + foreach (self::getAttributeMapper() as $key => $attribute) { + if (isset($attribute['parser'])) { + $route->{$attribute['name']} = self::parseDelegated($node, $key, $attribute); } } @@ -111,55 +96,19 @@ public static function parse($nodes) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Route $route, \DOMDocument &$document) + public static function toXML(Route $route, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); - foreach (self::$attributeMapper as $key => $attribute) { - if (!is_null($route->{$attribute['name']})) { - switch ($key) { - case 'links': - $child = LinkParser::toXMLArray($route->links, $document); - break; - case 'extensions': - $child = ExtensionParser::toXML($route->extensions, $document); - break; - case 'rtept': - $child = PointParser::toXMLArray($route->points, $document); - break; - default: - $child = $document->createElement($key); - $elementText = $document->createTextNode((string) $route->{$attribute['name']}); - $child->appendChild($elementText); - break; - } + self::mapAttributesToXML($route, $document, $node); - if (is_array($child)) { - foreach ($child as $item) { - $node->appendChild($item); - } - } else { - $node->appendChild($child); - } + foreach (self::getAttributeMapper() as $key => $attribute) { + if (isset($attribute['parser'])) { + self::serializeDelegated($route->{$attribute['name']}, $attribute, $document, $node); } } return $node; } - /** - * @param array $routes - * @param \DOMDocument $document - * @return \DOMElement[] - */ - public static function toXMLArray(array $routes, \DOMDocument &$document) - { - $result = []; - - foreach ($routes as $route) { - $result[] = self::toXML($route, $document); - } - - return $result; - } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/SegmentParser.php b/src/phpGPX/Parsers/SegmentParser.php index 33aa8e6..ff22afa 100644 --- a/src/phpGPX/Parsers/SegmentParser.php +++ b/src/phpGPX/Parsers/SegmentParser.php @@ -15,40 +15,38 @@ */ abstract class SegmentParser { - public static $tagName = 'trkseg'; + public static string $tagName = 'trkseg'; /** - * @param $nodes \SimpleXMLElement[] - * @return Segment[] + * Parse a single track segment node. + * + * @param \SimpleXMLElement $node + * @return Segment|null */ - public static function parse($nodes) + public static function parse(\SimpleXMLElement $node): ?Segment { - $segments = []; - - foreach ($nodes as $node) { - $segment = new Segment(); - - if (!$node->count()) { - continue; - } + if (!$node->count()) { + return null; + } - if (isset($node->trkpt)) { - $segment->points = []; + $segment = new Segment(); - foreach ($node->trkpt as $point) { - $segment->points[] = PointParser::parse($point); + if (isset($node->trkpt)) { + foreach ($node->trkpt as $point) { + $parsed = PointParser::parse($point); + if ($parsed !== null) { + $segment->points[] = $parsed; } } - $segment->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; + } - if (phpGPX::$CALCULATE_STATS) { - $segment->recalculateStats(); - } + $segment->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; - $segments[] = $segment; + if (phpGPX::$CALCULATE_STATS) { + $segment->recalculateStats(); } - return $segments; + return $segment; } /** @@ -56,7 +54,7 @@ public static function parse($nodes) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Segment $segment, \DOMDocument &$document) + public static function toXML(Segment $segment, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); @@ -70,20 +68,4 @@ public static function toXML(Segment $segment, \DOMDocument &$document) return $node; } - - /** - * @param array $segments - * @param \DOMDocument $document - * @return \DOMElement[] - */ - public static function toXMLArray(array $segments, \DOMDocument $document) - { - $result = []; - - foreach ($segments as $segment) { - $result[] = self::toXML($segment, $document); - } - - return $result; - } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/TrackParser.php b/src/phpGPX/Parsers/TrackParser.php index 8a77a67..ccf47a4 100644 --- a/src/phpGPX/Parsers/TrackParser.php +++ b/src/phpGPX/Parsers/TrackParser.php @@ -13,80 +13,71 @@ * Class TrackParser * @package phpGPX\Parsers */ -abstract class TrackParser +abstract class TrackParser extends AbstractParser { - public static $tagName = 'trk'; - - private static $attributeMapper = [ - 'name' => [ - 'name' => 'name', - 'type' => 'string' - ], - 'cmt' => [ - 'name' => 'comment', - 'type' => 'string' - ], - 'desc' => [ - 'name' => 'description', - 'type' => 'string' - ], - 'src' => [ - 'name' => 'source', - 'type' => 'string' - ], - 'link' => [ - 'name' => 'links', - 'type' => 'array' - ], - 'number' => [ - 'name' => 'number', - 'type' => 'integer' - ], - 'type' => [ - 'name' => 'type', - 'type' => 'string' - ], - 'extensions' => [ - 'name' => 'extensions', - 'type' => 'object' - ], - 'trkseg' => [ - 'name' => 'segments', - 'type' => 'array' - ], - ]; + public static string $tagName = 'trk'; + + protected static function getAttributeMapper(): array + { + return [ + 'name' => [ + 'name' => 'name', + 'type' => 'string' + ], + 'cmt' => [ + 'name' => 'comment', + 'type' => 'string' + ], + 'desc' => [ + 'name' => 'description', + 'type' => 'string' + ], + 'src' => [ + 'name' => 'source', + 'type' => 'string' + ], + 'link' => [ + 'name' => 'links', + 'type' => 'array', + 'parser' => LinkParser::class, + ], + 'number' => [ + 'name' => 'number', + 'type' => 'integer' + ], + 'type' => [ + 'name' => 'type', + 'type' => 'string' + ], + 'extensions' => [ + 'name' => 'extensions', + 'type' => 'object', + 'parser' => ExtensionParser::class, + ], + 'trkseg' => [ + 'name' => 'segments', + 'type' => 'array', + 'parser' => SegmentParser::class, + ], + ]; + } /** * @param \SimpleXMLElement $nodes * @return Track[] */ - public static function parse(\SimpleXMLElement $nodes) + public static function parse(\SimpleXMLElement $nodes): array { $tracks = []; foreach ($nodes as $node) { $track = new Track(); - foreach (self::$attributeMapper as $key => $attribute) { - switch ($key) { - case 'link': - $track->links = isset($node->link) ? LinkParser::parse($node->link) : []; - break; - case 'extensions': - $track->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; - break; - case 'trkseg': - $track->segments = isset($node->trkseg) ? SegmentParser::parse($node->trkseg) : []; - break; - default: - if (!in_array($attribute['type'], ['object', 'array'])) { - if (isset($node->$key)) { - $value = (string) $node->$key; - settype($value, $attribute['type']); - $track->{$attribute['name']} = $value; - } - } - break; + self::mapAttributesFromXML($node, $track); + + foreach (self::getAttributeMapper() as $key => $attribute) { + if (isset($attribute['parser'])) { + $track->{$attribute['name']} = self::parseDelegated($node, $key, $attribute); } } @@ -105,55 +96,19 @@ public static function parse(\SimpleXMLElement $nodes) * @param \DOMDocument $document * @return \DOMElement */ - public static function toXML(Track $track, \DOMDocument &$document) + public static function toXML(Track $track, \DOMDocument &$document): \DOMElement { $node = $document->createElement(self::$tagName); - foreach (self::$attributeMapper as $key => $attribute) { - if (!is_null($track->{$attribute['name']})) { - switch ($key) { - case 'link': - $child = LinkParser::toXMLArray($track->links, $document); - break; - case 'extensions': - $child = ExtensionParser::toXML($track->extensions, $document); - break; - case 'trkseg': - $child = SegmentParser::toXMLArray($track->segments, $document); - break; - default: - $child = $document->createElement($key); - $elementText = $document->createTextNode((string) $track->{$attribute['name']}); - $child->appendChild($elementText); - break; - } + self::mapAttributesToXML($track, $document, $node); - if (is_array($child)) { - foreach ($child as $item) { - $node->appendChild($item); - } - } else { - $node->appendChild($child); - } + foreach (self::getAttributeMapper() as $key => $attribute) { + if (isset($attribute['parser'])) { + self::serializeDelegated($track->{$attribute['name']}, $attribute, $document, $node); } } return $node; } - /** - * @param array $tracks - * @param \DOMDocument $document - * @return \DOMElement[] - */ - public static function toXMLArray(array $tracks, \DOMDocument &$document) - { - $result = []; - - foreach ($tracks as $track) { - $result[] = self::toXML($track, $document); - } - - return $result; - } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/WaypointParser.php b/src/phpGPX/Parsers/WaypointParser.php index 659af6f..f1ea104 100644 --- a/src/phpGPX/Parsers/WaypointParser.php +++ b/src/phpGPX/Parsers/WaypointParser.php @@ -17,7 +17,7 @@ abstract class WaypointParser * @param \SimpleXMLElement $nodes - a non empty list of wpt elements * @return array */ - public static function parse(\SimpleXMLElement $nodes) + public static function parse(\SimpleXMLElement $nodes): array { $points = []; diff --git a/tests/Unit/Parsers/LinkParserTest.php b/tests/Unit/Parsers/LinkParserTest.php index f5f8d63..f8c9326 100644 --- a/tests/Unit/Parsers/LinkParserTest.php +++ b/tests/Unit/Parsers/LinkParserTest.php @@ -25,12 +25,9 @@ protected function setUp(): void public function testParse(): void { - $links = LinkParser::parse($this->file->link); + $link = LinkParser::parse($this->file->link); - $this->assertNotEmpty($links); - $this->assertCount(1, $links); - - $link = $links[0]; + $this->assertInstanceOf(Link::class, $link); $this->assertEquals($this->link->href, $link->href); $this->assertEquals($this->link->text, $link->text); $this->assertEquals($this->link->type, $link->type); @@ -43,11 +40,7 @@ public function testToXML(): void $document = new \DOMDocument("1.0", 'UTF-8'); $root = $document->createElement("document"); - $xmlArray = LinkParser::toXMLArray([$this->link], $document); - - foreach ($xmlArray as $xmlElement) { - $root->appendChild($xmlElement); - } + $root->appendChild(LinkParser::toXML($this->link, $document)); $document->appendChild($root); From db7749e5004facdd94209b57d033901c5575949b Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 17:26:10 +0100 Subject: [PATCH 18/31] =?UTF-8?q?Instance=20based=20configuration=20handli?= =?UTF-8?q?ng=20(because=20I=20wanted=20it=20for=20some=20reason)=20?= =?UTF-8?q?=F0=9F=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/00_Getting_Started/02_Quick_Start.md | 14 +- docs/01_Usage/01_Loading_Files.md | 32 +++- docs/01_Usage/02_Creating_Files.md | 12 +- docs/01_Usage/03_Statistics.md | 36 +++-- docs/01_Usage/04_Configuration.md | 69 +++++--- docs/01_Usage/05_Extensions.md | 3 +- docs/02_Output_Formats/01_XML.md | 10 +- docs/02_Output_Formats/02_JSON.md | 100 +++++++----- docs/02_Output_Formats/03_GeoJSON.md | 72 +-------- docs/04_Development/03_Roadmap_2x.md | 149 ++++++++++-------- docs/index.md | 6 +- src/phpGPX/Config.php | 92 +++-------- src/phpGPX/Helpers/DistanceCalculator.php | 37 ++--- .../Helpers/ElevationGainLossCalculator.php | 27 ++-- src/phpGPX/Models/GpxFile.php | 66 ++------ src/phpGPX/Models/Point.php | 10 +- src/phpGPX/Models/Route.php | 25 +-- src/phpGPX/Models/Segment.php | 12 +- src/phpGPX/Models/Stats.php | 5 +- src/phpGPX/Models/StatsCalculator.php | 7 +- src/phpGPX/Models/Track.php | 14 +- src/phpGPX/Parsers/RouteParser.php | 5 - src/phpGPX/Parsers/SegmentParser.php | 5 - src/phpGPX/Parsers/TrackParser.php | 5 - src/phpGPX/phpGPX.php | 143 ++++++----------- tests/Integration/GeoJsonOutputTest.php | 20 ++- tests/Integration/GpxFileLoadTest.php | 19 ++- tests/Integration/XmlRoundTripTest.php | 27 ++-- tests/Unit/Helpers/DistanceCalculatorTest.php | 39 ++--- .../ElevationGainLossCalculatorTest.php | 58 ++++--- tests/Unit/Models/StatsCalculationTest.php | 43 +++-- 31 files changed, 527 insertions(+), 635 deletions(-) diff --git a/docs/00_Getting_Started/02_Quick_Start.md b/docs/00_Getting_Started/02_Quick_Start.md index 74cfdef..c439045 100644 --- a/docs/00_Getting_Started/02_Quick_Start.md +++ b/docs/00_Getting_Started/02_Quick_Start.md @@ -5,14 +5,16 @@ ```php use phpGPX\phpGPX; -$file = phpGPX::load('path/to/file.gpx'); +$gpx = new phpGPX(); +$file = $gpx->load('path/to/file.gpx'); ``` You can also parse GPX data from a string: ```php +$gpx = new phpGPX(); $xml = file_get_contents('path/to/file.gpx'); -$file = phpGPX::parse($xml); +$file = $gpx->parse($xml); ``` ## Accessing data @@ -51,14 +53,12 @@ foreach ($file->routes as $route) { ```php use phpGPX\phpGPX; -$file = phpGPX::load('input.gpx'); +$gpx = new phpGPX(); +$file = $gpx->load('input.gpx'); // Save as GPX XML $file->save('output.gpx', phpGPX::XML_FORMAT); -// Save as JSON -$file->save('output.json', phpGPX::JSON_FORMAT); - // Save as GeoJSON -$file->save('output.geojson', phpGPX::GEOJSON_FORMAT); +$file->save('output.geojson', phpGPX::JSON_FORMAT); ``` \ No newline at end of file diff --git a/docs/01_Usage/01_Loading_Files.md b/docs/01_Usage/01_Loading_Files.md index c2d4f2b..4b16473 100644 --- a/docs/01_Usage/01_Loading_Files.md +++ b/docs/01_Usage/01_Loading_Files.md @@ -7,7 +7,8 @@ The simplest way to load a GPX file: ```php use phpGPX\phpGPX; -$file = phpGPX::load('/path/to/track.gpx'); +$gpx = new phpGPX(); +$file = $gpx->load('/path/to/track.gpx'); ``` ## From string @@ -21,7 +22,24 @@ $xml = ' '; -$file = phpGPX::parse($xml); +$gpx = new phpGPX(); +$file = $gpx->parse($xml); +``` + +## With custom configuration + +Pass a `Config` object to customize parsing behavior: + +```php +use phpGPX\phpGPX; +use phpGPX\Config; + +$gpx = new phpGPX(new Config( + calculateStats: false, + sortByTimestamp: true, +)); + +$file = $gpx->load('/path/to/track.gpx'); ``` ## What gets parsed @@ -41,14 +59,18 @@ By default, statistics are calculated automatically when loading a file. This in To disable automatic stats calculation: ```php -phpGPX::$CALCULATE_STATS = false; +use phpGPX\Config; -$file = phpGPX::load('track.gpx'); +$gpx = new phpGPX(new Config(calculateStats: false)); +$file = $gpx->load('track.gpx'); // $file->tracks[0]->stats will be null ``` You can recalculate stats manually at any time: ```php -$file->tracks[0]->recalculateStats(); +use phpGPX\Config; + +$config = new Config(); +$file->tracks[0]->recalculateStats($config); ``` \ No newline at end of file diff --git a/docs/01_Usage/02_Creating_Files.md b/docs/01_Usage/02_Creating_Files.md index d87b045..d31416e 100644 --- a/docs/01_Usage/02_Creating_Files.md +++ b/docs/01_Usage/02_Creating_Files.md @@ -5,6 +5,7 @@ You can build GPX files programmatically. ## Building a track from scratch ```php +use phpGPX\Config; use phpGPX\Models\GpxFile; use phpGPX\Models\Metadata; use phpGPX\Models\Point; @@ -45,7 +46,7 @@ foreach ($points as $data) { $track->segments[] = $segment; // Calculate statistics -$track->recalculateStats(); +$track->recalculateStats(new Config()); $gpxFile->tracks[] = $track; @@ -56,9 +57,11 @@ $gpxFile->save('morning_run.gpx', phpGPX::XML_FORMAT); ## Building a route ```php +use phpGPX\Config; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; use phpGPX\Models\Route; +use phpGPX\phpGPX; $gpxFile = new GpxFile(); @@ -80,10 +83,10 @@ foreach ($waypoints as $data) { $route->points[] = $point; } -$route->recalculateStats(); +$route->recalculateStats(new Config()); $gpxFile->routes[] = $route; -$gpxFile->save('trail.gpx', \phpGPX\phpGPX::XML_FORMAT); +$gpxFile->save('trail.gpx', phpGPX::XML_FORMAT); ``` ## Adding waypoints @@ -92,6 +95,7 @@ $gpxFile->save('trail.gpx', \phpGPX\phpGPX::XML_FORMAT); use phpGPX\Models\GpxFile; use phpGPX\Models\Link; use phpGPX\Models\Point; +use phpGPX\phpGPX; $gpxFile = new GpxFile(); @@ -109,7 +113,7 @@ $link->text = "Official website"; $waypoint->links[] = $link; $gpxFile->waypoints[] = $waypoint; -$gpxFile->save('places.gpx', \phpGPX\phpGPX::XML_FORMAT); +$gpxFile->save('places.gpx', phpGPX::XML_FORMAT); ``` ## Direct XML output to browser diff --git a/docs/01_Usage/03_Statistics.md b/docs/01_Usage/03_Statistics.md index b9d2d98..0664045 100644 --- a/docs/01_Usage/03_Statistics.md +++ b/docs/01_Usage/03_Statistics.md @@ -25,7 +25,10 @@ Coordinate properties are also available: `startedAtCoords`, `finishedAtCoords`, ## Accessing statistics ```php -$file = phpGPX::load('track.gpx'); +use phpGPX\phpGPX; + +$gpx = new phpGPX(); +$file = $gpx->load('track.gpx'); foreach ($file->tracks as $track) { $stats = $track->stats; @@ -45,10 +48,13 @@ foreach ($file->tracks as $track) { ## Recalculating statistics -After modifying points, recalculate: +After modifying points, recalculate by passing a `Config` object: ```php -$track->recalculateStats(); +use phpGPX\Config; + +$config = new Config(); +$track->recalculateStats($config); ``` For tracks, this recalculates each segment's stats first, then aggregates them. @@ -58,8 +64,12 @@ For tracks, this recalculates each segment's stats first, then aggregates them. GPS noise can inflate distance measurements. Enable smoothing to filter out small movements: ```php -phpGPX::$APPLY_DISTANCE_SMOOTHING = true; -phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; // meters — ignore movements smaller than this +use phpGPX\Config; + +$gpx = new phpGPX(new Config( + applyDistanceSmoothing: true, + distanceSmoothingThreshold: 2, // meters — ignore movements smaller than this +)); ``` ## Elevation smoothing @@ -67,11 +77,15 @@ phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; // meters — ignore movements smalle GPS altitude data is often noisy. Smoothing helps get more accurate elevation gain/loss: ```php -phpGPX::$APPLY_ELEVATION_SMOOTHING = true; -phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; // meters — minimum change to count +use phpGPX\Config; -// Optional: filter spikes (e.g. GPS glitches showing 100m jumps) -phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = 50; // meters — maximum change to count +$gpx = new phpGPX(new Config( + applyElevationSmoothing: true, + elevationSmoothingThreshold: 2, // meters — minimum change to count + + // Optional: filter spikes (e.g. GPS glitches showing 100m jumps) + elevationSmoothingSpikesThreshold: 50, // meters — maximum change to count +)); ``` ## Ignoring zero elevation @@ -79,5 +93,7 @@ phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = 50; // meters — maximum change Some GPS devices record elevation as 0 when they lose satellite fix. Ignore these points: ```php -phpGPX::$IGNORE_ELEVATION_0 = true; +use phpGPX\Config; + +$gpx = new phpGPX(new Config(ignoreZeroElevation: true)); ``` \ No newline at end of file diff --git a/docs/01_Usage/04_Configuration.md b/docs/01_Usage/04_Configuration.md index 8e777db..97db14f 100644 --- a/docs/01_Usage/04_Configuration.md +++ b/docs/01_Usage/04_Configuration.md @@ -1,42 +1,65 @@ # Configuration -phpGPX is configured through static properties on the `phpGPX` class. Set these before loading or creating files. +phpGPX is configured through the `Config` value object, passed to the `phpGPX` constructor. Each instance carries its own configuration — there is no global state. ## All options ```php use phpGPX\phpGPX; +use phpGPX\Config; -// Calculate statistics automatically on load (default: true) -phpGPX::$CALCULATE_STATS = true; +$gpx = new phpGPX(new Config( + // Calculate statistics automatically on load (default: true) + calculateStats: true, -// Sort points by timestamp when loading (default: false) -phpGPX::$SORT_BY_TIMESTAMP = false; + // Sort points by timestamp when loading (default: false) + sortByTimestamp: false, -// DateTime format for JSON output (default: 'c' — ISO 8601) -phpGPX::$DATETIME_FORMAT = 'c'; + // Pretty print XML and JSON output (default: true) + prettyPrint: true, -// Timezone for DateTime output (default: null — uses UTC) -phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'UTC'; + // Ignore elevation values of 0 in stats (default: false) + ignoreZeroElevation: false, -// Pretty print XML and JSON output (default: true) -phpGPX::$PRETTY_PRINT = true; + // Distance smoothing (default: false) + applyDistanceSmoothing: false, + distanceSmoothingThreshold: 2, // meters -// Ignore elevation values of 0 in stats (default: false) -phpGPX::$IGNORE_ELEVATION_0 = false; + // Elevation smoothing (default: false) + applyElevationSmoothing: false, + elevationSmoothingThreshold: 2, // meters + elevationSmoothingSpikesThreshold: null, // meters, or null to disable +)); +``` + +## Default configuration + +All options have sensible defaults. Creating a `phpGPX` instance without arguments uses them: + +```php +$gpx = new phpGPX(); // uses all defaults +``` + +## Multiple configurations + +Since configuration is per-instance, you can use different settings for different files: + +```php +$smooth = new phpGPX(new Config( + applyElevationSmoothing: true, + elevationSmoothingThreshold: 5, +)); -// Distance smoothing (default: false) -phpGPX::$APPLY_DISTANCE_SMOOTHING = false; -phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; // meters +$raw = new phpGPX(new Config( + applyElevationSmoothing: false, +)); -// Elevation smoothing (default: false) -phpGPX::$APPLY_ELEVATION_SMOOTHING = false; -phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; // meters -phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; // meters, or null to disable +$smoothFile = $smooth->load('track.gpx'); +$rawFile = $raw->load('track.gpx'); ``` ## Notes -- All configuration is global via static properties. There is no per-file configuration. -- Settings affect both loading (parsing + stats calculation) and saving (serialization format). -- The `$SORT_BY_TIMESTAMP` option is useful for GPX files where points are out of order, but is disabled by default since most files are already sorted. \ No newline at end of file +- Configuration is immutable after construction — `Config` properties are set once via constructor. +- The `sortByTimestamp` option is useful for GPX files where points are out of order, but is disabled by default since most files are already sorted. +- JSON output always uses ISO 8601 UTC for datetime values (GeoJSON convention). Datetime formatting is a consumer concern. \ No newline at end of file diff --git a/docs/01_Usage/05_Extensions.md b/docs/01_Usage/05_Extensions.md index 4d23a78..af5b5fb 100644 --- a/docs/01_Usage/05_Extensions.md +++ b/docs/01_Usage/05_Extensions.md @@ -22,7 +22,8 @@ The most common extension. Provides sensor data per track point. ### Reading extensions ```php -$file = phpGPX::load('garmin_track.gpx'); +$gpx = new phpGPX(); +$file = $gpx->load('garmin_track.gpx'); foreach ($file->tracks as $track) { foreach ($track->segments as $segment) { diff --git a/docs/02_Output_Formats/01_XML.md b/docs/02_Output_Formats/01_XML.md index 5903615..f02eb6d 100644 --- a/docs/02_Output_Formats/01_XML.md +++ b/docs/02_Output_Formats/01_XML.md @@ -17,13 +17,15 @@ $xmlString = $document->saveXML(); ## Pretty printing -By default, XML output is formatted with indentation: +By default, XML output is formatted with indentation. Disable it via Config: ```php -phpGPX::$PRETTY_PRINT = true; // default -``` +use phpGPX\Config; -Set to `false` for compact output. +$gpx = new phpGPX(new Config(prettyPrint: false)); +$file = $gpx->load('input.gpx'); +$file->save('compact.gpx', phpGPX::XML_FORMAT); +``` ## Namespaces diff --git a/docs/02_Output_Formats/02_JSON.md b/docs/02_Output_Formats/02_JSON.md index 23b5c97..e07f011 100644 --- a/docs/02_Output_Formats/02_JSON.md +++ b/docs/02_Output_Formats/02_JSON.md @@ -1,68 +1,94 @@ -# JSON +# JSON (GeoJSON) -A structured JSON representation that mirrors the GPX data model. +phpGPX outputs JSON in [GeoJSON](https://geojson.org/) format (RFC 7946). Both `JSON_FORMAT` and `GEOJSON_FORMAT` produce identical GeoJSON output. ## Saving to file ```php $file->save('output.json', phpGPX::JSON_FORMAT); +// or equivalently: +$file->save('output.geojson', phpGPX::GEOJSON_FORMAT); ``` ## Getting JSON as string ```php -$jsonString = $file->toJSON(false); // false = GPX structure format +$geoJsonString = $file->toJSON(); ``` ## Structure -The JSON output follows the GPX structure: +The output is a GeoJSON `FeatureCollection`: ```json { - "creator": "phpGPX/2.0.0-alpha.1", - "metadata": { - "name": "My Track", - "time": "2024-01-15T07:00:00+00:00" - }, - "tracks": [ + "type": "FeatureCollection", + "features": [ { - "name": "Morning Run", - "trkseg": [ - { - "points": [ - { - "lat": 48.157, - "lon": 17.054, - "ele": 134.0, - "time": "2024-01-15T07:00:00+00:00" - } - ], - "stats": { - "distance": 1250.5, - "avgSpeed": 3.2 - } - } - ], - "stats": { } + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [17.054, 48.157, 134.0] + }, + "properties": { + "name": "Waypoint 1", + "ele": 134.0, + "time": "2024-01-15T07:00:00+00:00" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiLineString", + "coordinates": [ + [[17.054, 48.157, 134.0], [17.055, 48.158, 136.0]] + ] + }, + "properties": { + "name": "Morning Run", + "stats": { "distance": 1250.5 } + } } - ] + ], + "properties": { + "metadata": { "name": "My Track" }, + "creator": "phpGPX/2.0.0-alpha.2" + } } ``` +## Geometry type mapping + +| GPX element | GeoJSON geometry | +|-------------|-----------------| +| Waypoint (``) | `Point` | +| Route (``) | `LineString` | +| Track (``) | `MultiLineString` (one line per segment) | + +## Coordinate order + +GeoJSON uses `[longitude, latitude, elevation]` order, which is different from the GPX `lat/lon` order. phpGPX handles this conversion automatically. + ## DateTime format -Control the DateTime output format: +All datetime values are serialized as ISO 8601 UTC strings (e.g. `2024-01-15T07:00:00+00:00`). This is the industry standard for GeoJSON and data interchange formats. If you need a different format for display, transform the dates on the consumer side. + +## Pretty printing + +Control JSON formatting via Config: ```php -phpGPX::$DATETIME_FORMAT = 'c'; // ISO 8601 (default) -phpGPX::$DATETIME_FORMAT = 'U'; // Unix timestamp -phpGPX::$DATETIME_FORMAT = 'Y-m-d H:i:s'; // Custom +use phpGPX\Config; + +$gpx = new phpGPX(new Config(prettyPrint: false)); +$file = $gpx->load('track.gpx'); +echo $file->toJSON(); // compact, single-line JSON ``` -## Timezone +## Using with Leaflet -```php -phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'UTC'; // default -phpGPX::$DATETIME_TIMEZONE_OUTPUT = 'Europe/Prague'; // local time +```javascript +fetch('output.geojson') + .then(r => r.json()) + .then(data => L.geoJSON(data).addTo(map)); ``` \ No newline at end of file diff --git a/docs/02_Output_Formats/03_GeoJSON.md b/docs/02_Output_Formats/03_GeoJSON.md index a3a6238..516ea0f 100644 --- a/docs/02_Output_Formats/03_GeoJSON.md +++ b/docs/02_Output_Formats/03_GeoJSON.md @@ -1,73 +1,5 @@ # GeoJSON -phpGPX can output data in [GeoJSON](https://geojson.org/) format (RFC 7946), which is widely supported by mapping libraries like Leaflet, Mapbox, and OpenLayers. +See [JSON (GeoJSON)](02_JSON.md) — in phpGPX 2.x, JSON output is always GeoJSON (RFC 7946). -## Saving to file - -```php -$file->save('output.geojson', phpGPX::GEOJSON_FORMAT); -``` - -## Getting GeoJSON as string - -```php -$geoJsonString = $file->toJSON(true); // true = GeoJSON format -``` - -## Structure - -The output is a GeoJSON `FeatureCollection`: - -```json -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [17.054, 48.157, 134.0] - }, - "properties": { - "name": "Waypoint 1", - "ele": 134.0, - "time": "2024-01-15T07:00:00+00:00" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiLineString", - "coordinates": [ - [[17.054, 48.157, 134.0], [17.055, 48.158, 136.0]] - ] - }, - "properties": { - "name": "Morning Run", - "stats": { "distance": 1250.5 } - } - } - ], - "metadata": { } -} -``` - -## Geometry type mapping - -| GPX element | GeoJSON geometry | -|-------------|-----------------| -| Waypoint (``) | `Point` | -| Route (``) | `LineString` | -| Track (``) | `MultiLineString` (one line per segment) | - -## Coordinate order - -GeoJSON uses `[longitude, latitude, elevation]` order, which is different from the GPX `lat/lon` order. phpGPX handles this conversion automatically. - -## Using with Leaflet - -```javascript -fetch('output.geojson') - .then(r => r.json()) - .then(data => L.geoJSON(data).addTo(map)); -``` \ No newline at end of file +Both `phpGPX::JSON_FORMAT` and `phpGPX::GEOJSON_FORMAT` produce the same GeoJSON `FeatureCollection` output. \ No newline at end of file diff --git a/docs/04_Development/03_Roadmap_2x.md b/docs/04_Development/03_Roadmap_2x.md index f3de36d..427c0c9 100644 --- a/docs/04_Development/03_Roadmap_2x.md +++ b/docs/04_Development/03_Roadmap_2x.md @@ -50,85 +50,98 @@ The `develop` branch is the home of all 2.x work. --- -## Phase 2: Instance-Based `phpGPX` Entry Point - -**Goal:** Replace global static configuration with an instance that carries its own settings and middleware. - -### 2.1 — Convert `phpGPX` to an instance class (#68) - -Current (static, global state): -```php -phpGPX::$CALCULATE_STATS = true; -phpGPX::$APPLY_ELEVATION_SMOOTHING = true; -$file = phpGPX::load('track.gpx'); -``` - -New (instance, injectable): -```php -$gpx = new phpGPX(); -$gpx->addMiddleware(new StatsMiddleware()); -$gpx->addMiddleware(new ElevationSmoothingMiddleware(threshold: 2, spikesThreshold: 5)); -$file = $gpx->load('track.gpx'); -``` - -Keep a static convenience `phpGPX::load()` that creates a default-configured instance for -backward-compatible simple usage. - -**Files to change:** -- `src/phpGPX/phpGPX.php` (refactor) -- `src/phpGPX/Config.php` (new — holds config as a value object instead of static properties) - -### 2.2 — Move stats calculation out of parsers - -Currently `TrackParser::parse()` calls `$track->recalculateStats()` inside the parse loop, -gated by `phpGPX::$CALCULATE_STATS`. This couples parsing to stats computation. - -Move stats calculation into a `StatsMiddleware` that runs after parsing is complete. -Parsers should only produce the model tree from XML — nothing else. +### Phase 2: Instance-Based `phpGPX` Entry Point + +- [x] **2.1 — `Config` value object** + Created `Config` class with constructor promotion. All settings are explicit, typed, documented. + Removed `datetimeFormat` and `datetimeTimezone` — GeoJSON always outputs ISO 8601 UTC + per industry convention (RFC 7946, Mapbox, GDAL). Datetime formatting is a consumer concern. + + Final Config properties (9): + `calculateStats`, `sortByTimestamp`, `prettyPrint`, `ignoreZeroElevation`, + `applyElevationSmoothing`, `elevationSmoothingThreshold`, `elevationSmoothingSpikesThreshold`, + `applyDistanceSmoothing`, `distanceSmoothingThreshold`. + +- [x] **2.2 — Instance-based `phpGPX` class (#68)** + Rewrote `phpGPX` as an instance class. No static properties. `load()` and `parse()` are + instance methods. Config passed via constructor: `new phpGPX(new Config(...))`. + Only `getSignature()` and format constants remain static (stateless). + No legacy static bridge — clean break from 1.x. + +- [x] **2.3 — Stats calculation moved out of parsers** + Removed `recalculateStats()` calls from TrackParser, RouteParser, SegmentParser. + Parsers only produce the model tree from XML — no side effects. + `phpGPX::parse()` handles post-processing in two steps: + 1. `sortByTimestamp` — sorts point arrays in-place via `DateTimeHelper::comparePointsByTimestamp` + 2. `calculateStats` — calls `recalculateStats(Config)` on each track and route + +- [x] **2.4 — Config threaded through entire model/helper chain** + `StatsCalculator::recalculateStats(Config $config)` — interface updated. + `Segment`, `Route`, `Track` — `recalculateStats()` accepts Config, passes it to: + - `ElevationGainLossCalculator::calculate(points, config)` — uses `ignoreZeroElevation`, + `applyElevationSmoothing`, `elevationSmoothingThreshold`, `elevationSmoothingSpikesThreshold` + - `DistanceCalculator::__construct(points, config)` — uses `applyDistanceSmoothing`, + `distanceSmoothingThreshold` + - Min/max altitude loops use `config->ignoreZeroElevation` + `GpxFile` — holds Config via constructor, uses `prettyPrint` for XML/JSON output. + `Point`, `Stats` — no Config needed. DateTime serialization uses hardcoded ISO 8601 UTC defaults. + +- [x] **2.5 — All tests updated** + Unit tests (DistanceCalculatorTest, ElevationGainLossCalculatorTest, StatsCalculationTest) + use `new Config(...)` with named arguments instead of static property mutation. + Integration tests (GpxFileLoadTest, XmlRoundTripTest, GeoJsonOutputTest) use `new phpGPX()`. + 86 tests, 356 assertions — all passing. + + Config property verification — every property is declared, used, and tested: + | Property | Consumer | + |---|---| + | `calculateStats` | `phpGPX::parse()` | + | `sortByTimestamp` | `phpGPX::sortPointsByTimestamp()` | + | `prettyPrint` | `GpxFile::toJSON()`, `GpxFile::toXML()` | + | `ignoreZeroElevation` | `ElevationGainLossCalculator`, `Segment/Route::recalculateStats()` | + | `applyElevationSmoothing` | `ElevationGainLossCalculator` | + | `elevationSmoothingThreshold` | `ElevationGainLossCalculator` | + | `elevationSmoothingSpikesThreshold` | `ElevationGainLossCalculator` | + | `applyDistanceSmoothing` | `DistanceCalculator` | + | `distanceSmoothingThreshold` | `DistanceCalculator` | --- ## Phase 3: Middleware System (#68) -**Goal:** Composable post-parse processing pipeline. +**Goal:** Composable post-parse processing pipeline for features that go beyond Config flags. + +> **Note:** Phase 2 already eliminated all static config properties and replaced the core +> processing flags (stats, smoothing, sorting) with the `Config` value object. Middleware +> is for new, composable features that don't fit as simple boolean flags. ### 3.1 — Define `MiddlewareInterface` ```php interface MiddlewareInterface { - public function process(GpxFile $gpxFile): GpxFile; + public function process(GpxFile $gpxFile, Config $config): GpxFile; } ``` -### 3.2 — Implement core middlewares +### 3.2 — Implement middlewares for new features -| Middleware | Replaces | GitHub Issue | +| Middleware | Purpose | GitHub Issue | |---|---|---| -| `StatsMiddleware` | `phpGPX::$CALCULATE_STATS` + `StatsCalculator` interface | — | -| `ElevationSmoothingMiddleware` | `phpGPX::$APPLY_ELEVATION_SMOOTHING` + threshold constants | #59 | -| `DistanceSmoothingMiddleware` | `phpGPX::$APPLY_DISTANCE_SMOOTHING` + threshold constant | — | -| `BoundsMiddleware` | Manual bounds — auto-compute for tracks/routes/segments | #28 | -| `TimestampSortMiddleware` | `phpGPX::$SORT_BY_TIMESTAMP` | — | -| `TrackPointExtensionStatsMiddleware` | Not yet implemented — stats from extension data (HR, cadence) | #15 | -| `MovementDurationMiddleware` | Not yet implemented — exclude pauses from duration/speed | Discussion #73 | +| `BoundsMiddleware` | Auto-compute coordinate bounds for tracks/routes/segments | #28 | +| `TrackPointExtensionStatsMiddleware` | Aggregate stats from extension data (HR, cadence, power) | #15 | +| `MovementDurationMiddleware` | Exclude pauses from duration/speed calculations | Discussion #73 | -### 3.3 — Default middleware stack +### 3.3 — Middleware pipeline in `phpGPX` -The default `phpGPX()` instance ships with: ```php -[ - new StatsMiddleware(), -] +$gpx = new phpGPX(); +$gpx->addMiddleware(new BoundsMiddleware()); +$gpx->addMiddleware(new MovementDurationMiddleware(pauseThreshold: 30)); +$file = $gpx->load('track.gpx'); ``` -Users opt-in to everything else explicitly. This replaces the current approach where -`CALCULATE_STATS`, `APPLY_ELEVATION_SMOOTHING`, etc. are global boolean flags. - -### 3.4 — Deprecate and remove static config properties - -After middlewares are stable, remove the static properties from `phpGPX` class. -The `Config` value object replaces format-related settings (PRETTY_PRINT, DATETIME_FORMAT, etc.). +Middlewares run after parsing and after built-in Config-driven processing (sorting, stats). --- @@ -207,10 +220,11 @@ Consider making `Stats` immutable — constructed by `StatsMiddleware`, not muta The `reset()` + incremental mutation pattern is fragile. A builder or factory approach within the middleware is cleaner. -### 5.4 — Remove `StatsCalculator` interface from models +### 5.4 — Evaluate `StatsCalculator` interface on models -Once stats computation moves to `StatsMiddleware`, models no longer need `recalculateStats()` -or `getPoints()` on the `StatsCalculator` interface. `getPoints()` can stay as a convenience +Stats computation is currently driven by `phpGPX::parse()` calling `recalculateStats(Config)` +on models. Consider whether the interface should remain (allows manual re-calculation by users) +or be removed in favor of a standalone stats service. `getPoints()` can stay as a convenience method on `Collection` without being interface-mandated. ### 5.5 — Fix startedAt/finishedAt for missing timestamps (#51) @@ -232,8 +246,11 @@ Evaluate phpDocumentor vs alternatives. Set up automated doc builds in CI. Document all breaking changes: - Removed `Summarizable` / `toArray()` — use `jsonSerialize()` - Removed `GpxSerializable` -- Instance-based API vs static methods -- Middleware system vs static config flags +- Instance-based API: `new phpGPX()` instead of static `phpGPX::load()` +- `Config` value object instead of static config flags (`phpGPX::$CALCULATE_STATS`, etc.) +- `recalculateStats(Config $config)` — requires Config parameter +- JSON output is GeoJSON (RFC 7946) — no configurable datetime format (always ISO 8601 UTC) +- Middleware system for extensible post-processing - Extension registry vs hardcoded extensions - `PointType` enum vs string constants @@ -246,8 +263,6 @@ Rewrite Getting Started, Usage, Configuration, Extensions sections to reflect 2. Benchmark parsing and serialization of large GPX files. Ensure no regressions from the architectural changes. -### 6.5 — Tag `2.0.0-beta.1`, then `2.0.0` - --- ## Issue Tracker Cross-Reference @@ -258,8 +273,8 @@ from the architectural changes. | #28 | Statistics - get Bounds of GPX Routes | Phase 3 (BoundsMiddleware) | | #41 | Implementing waypoint and creation time extensions | Phase 4 (Extension registry) | | #51 | startedAt/finishedAt missing timestamps | Phase 5.5 | -| #59 | Elevation gain/loss accuracy | Phase 3 (ElevationSmoothingMiddleware) | -| #68 | Middlewares | Phase 2 + 3 | +| #59 | Elevation gain/loss accuracy | Completed (Config-driven smoothing in Phase 2) | +| #68 | Middlewares | Phase 2 (Config) + Phase 3 (pipeline) | | #69 | Removal of Summarizable and toArray | Completed | | #70 | Min altitude not necessarily first point | Completed (verify) | | #72 | Add GPX version attribute | Phase 4.3 | diff --git a/docs/index.md b/docs/index.md index 68a3216..5fa0c2b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,14 +11,16 @@ A PHP library for reading, creating, and manipulating [GPX files](https://en.wik - Full support of [GPX 1.1 specification](http://www.topografix.com/GPX/1/1/) - Statistics calculation (distance, elevation, speed, pace, duration) - Extension support (Garmin TrackPointExtension) -- Output in XML, JSON, and GeoJSON formats +- GeoJSON output (RFC 7946) and GPX XML output +- Instance-based API with injectable configuration ## Quick Example ```php use phpGPX\phpGPX; -$file = phpGPX::load('track.gpx'); +$gpx = new phpGPX(); +$file = $gpx->load('track.gpx'); foreach ($file->tracks as $track) { echo $track->stats->distance . " meters\n"; diff --git a/src/phpGPX/Config.php b/src/phpGPX/Config.php index 4e001c8..9ecb381 100644 --- a/src/phpGPX/Config.php +++ b/src/phpGPX/Config.php @@ -2,81 +2,39 @@ namespace phpGPX; +/** + * Class Config + * Configuration value object for a phpGPX instance. + * @package phpGPX + */ class Config { - /** - * Create Stats object for each track, segment and route - * @var bool - */ - public bool $calculateStats = true; + public function __construct( + /** Calculate stats for tracks, segments and routes */ + public bool $calculateStats = true, - /** - * Additional sort based on timestamp in Routes & Tracks on XML read. - * Disabled by default, data should be already sorted. - * @var bool - */ - public bool $sortByTimeStamp = false; + /** Sort points by timestamp in Routes & Tracks on XML read */ + public bool $sortByTimestamp = false, - /** - * Default DateTime output format in JSON serialization. - * @var string - */ - public string $datetimeFormat = 'c'; + /** Pretty print XML and JSON output */ + public bool $prettyPrint = true, - /** - * Default timezone for display. - * Data are always stored in UTC timezone. - * @var string - */ - public string $datetimeTimezone = 'UTC'; + /** Ignore points with elevation of 0 in stats calculation */ + public bool $ignoreZeroElevation = false, - /** - * Pretty print. - * @var bool - */ - public bool $jsonPrettyPrint = true; + /** Apply elevation gain/loss smoothing */ + public bool $applyElevationSmoothing = false, - /** - * In stats elevation calculation: ignore points with an elevation of 0 - * This can happen with some GPS software adding a point with 0 elevation - * - * @var bool - */ - public bool $ignoreZeroElevation = true; + /** Minimum elevation difference in meters for smoothing */ + public int $elevationSmoothingThreshold = 2, - /** - * Apply elevation gain/loss smoothing? If true, the threshold in - * ELEVATION_SMOOTHING_THRESHOLD and ELEVATION_SMOOTHING_SPIKES_THRESHOLD (if not null) applies - * @var bool - */ - public bool $applyElevationSmoothing = false; + /** Maximum elevation difference in meters for spike filtering */ + public ?int $elevationSmoothingSpikesThreshold = null, - /** - * if APPLY_ELEVATION_SMOOTHING is true - * the minimum elevation difference between considered points in meters - * @var int - */ - public int $elevationSmoothingThreshold = 2; - - /** - * if APPLY_ELEVATION_SMOOTHING is true - * the maximum elevation difference between considered points in meters - * @var int|null - */ - public ?int $elevationSmoothingSpikesThreshold = null; - - /** - * Apply distance calculation smoothing? If true, the threshold in - * DISTANCE_SMOOTHING_THRESHOLD applies - * @var bool - */ - public bool $applyDistanceSmoothing = false; - - /** - * if APPLY_DISTANCE_SMOOTHING is true - * the minimum distance between considered points in meters - * @var int - */ - public int $distanceSmoothingThreshold = 2; + /** Apply distance calculation smoothing */ + public bool $applyDistanceSmoothing = false, + /** Minimum distance in meters between considered points for smoothing */ + public int $distanceSmoothingThreshold = 2, + ) {} } \ No newline at end of file diff --git a/src/phpGPX/Helpers/DistanceCalculator.php b/src/phpGPX/Helpers/DistanceCalculator.php index a72829e..1819317 100644 --- a/src/phpGPX/Helpers/DistanceCalculator.php +++ b/src/phpGPX/Helpers/DistanceCalculator.php @@ -10,41 +10,38 @@ namespace phpGPX\Helpers; +use phpGPX\Config; use phpGPX\Models\Point; -use phpGPX\phpGPX; class DistanceCalculator { - /** - * @var Point[] - */ + /** @var Point[] */ private array $points; + private Config $config; + /** - * DistanceCalculator constructor. * @param Point[] $points + * @param Config $config */ - public function __construct(array $points) + public function __construct(array $points, Config $config) { $this->points = $points; + $this->config = $config; } public function getRawDistance(): float - { + { return $this->calculate([GeoHelper::class, 'getRawDistance']); } public function getRealDistance(): float - { + { return $this->calculate([GeoHelper::class, 'getRealDistance']); } - /** - * @param array $strategy - * @return float - */ private function calculate(array $strategy): float - { + { $distance = 0; $pointCount = count($this->points); @@ -54,27 +51,21 @@ private function calculate(array $strategy): float for ($p = 0; $p < $pointCount; $p++) { $curPoint = $this->points[$p]; - // skip the first point if ($p === 0) { $lastConsideredPoint = $curPoint; continue; } - // calculate the delta from current point to last considered point $curPoint->difference = call_user_func($strategy, $lastConsideredPoint, $curPoint); - // if smoothing is applied we only consider points with a delta above the threshold (e.g. 2 meters) - if (phpGPX::$APPLY_DISTANCE_SMOOTHING) { + if ($this->config->applyDistanceSmoothing) { $differenceFromLastConsideredPoint = call_user_func($strategy, $curPoint, $lastConsideredPoint); - if ($differenceFromLastConsideredPoint > phpGPX::$DISTANCE_SMOOTHING_THRESHOLD) { + if ($differenceFromLastConsideredPoint > $this->config->distanceSmoothingThreshold) { $distance += $differenceFromLastConsideredPoint; $lastConsideredPoint = $curPoint; } - } - - // if smoothing is not applied we consider every point - else { + } else { $distance += $curPoint->difference; $lastConsideredPoint = $curPoint; } @@ -84,4 +75,4 @@ private function calculate(array $strategy): float return $distance; } -} +} \ No newline at end of file diff --git a/src/phpGPX/Helpers/ElevationGainLossCalculator.php b/src/phpGPX/Helpers/ElevationGainLossCalculator.php index 3304731..9af1ef1 100644 --- a/src/phpGPX/Helpers/ElevationGainLossCalculator.php +++ b/src/phpGPX/Helpers/ElevationGainLossCalculator.php @@ -8,17 +8,18 @@ namespace phpGPX\Helpers; +use phpGPX\Config; use phpGPX\Models\Point; -use phpGPX\phpGPX; class ElevationGainLossCalculator { /** * @param Point[] $points - * @return array + * @param Config $config + * @return array [cumulativeElevationGain, cumulativeElevationLoss] */ - public static function calculate(array $points): array - { + public static function calculate(array $points, Config $config): array + { $cumulativeElevationGain = 0; $cumulativeElevationLoss = 0; @@ -29,37 +30,31 @@ public static function calculate(array $points): array for ($p = 0; $p < $pointCount; $p++) { $curElevation = $points[$p]->elevation; - // skip points with empty elevation if ($curElevation === null) { continue; } - // skip points with 0 elevation if configuration allows - if (phpGPX::$IGNORE_ELEVATION_0 && $curElevation == 0) { + if ($config->ignoreZeroElevation && $curElevation == 0) { continue; } - // skip the first point if ($p === 0) { $lastConsideredElevation = $curElevation; continue; } - // calculate the delta from current point to last considered point $elevationDelta = $curElevation - $lastConsideredElevation; - // if smoothing is applied we only consider points with a delta above the threshold (e.g. 2 meters) - if (phpGPX::$APPLY_ELEVATION_SMOOTHING && - abs($elevationDelta) > phpGPX::$ELEVATION_SMOOTHING_THRESHOLD && - (phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD === null || abs($elevationDelta) < phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD)) { + if ($config->applyElevationSmoothing && + abs($elevationDelta) > $config->elevationSmoothingThreshold && + ($config->elevationSmoothingSpikesThreshold === null || abs($elevationDelta) < $config->elevationSmoothingSpikesThreshold)) { $cumulativeElevationGain += ($elevationDelta > 0) ? $elevationDelta : 0; $cumulativeElevationLoss += ($elevationDelta < 0) ? abs($elevationDelta) : 0; $lastConsideredElevation = $curElevation; } - // if smoothing is not applied we consider every point - if (!phpGPX::$APPLY_ELEVATION_SMOOTHING) { + if (!$config->applyElevationSmoothing) { $cumulativeElevationGain += ($elevationDelta > 0) ? $elevationDelta : 0; $cumulativeElevationLoss += ($elevationDelta < 0) ? abs($elevationDelta) : 0; @@ -69,4 +64,4 @@ public static function calculate(array $points): array return [$cumulativeElevationGain, $cumulativeElevationLoss]; } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php index 58c033a..1e81ada 100644 --- a/src/phpGPX/Models/GpxFile.php +++ b/src/phpGPX/Models/GpxFile.php @@ -6,6 +6,7 @@ namespace phpGPX\Models; +use phpGPX\Config; use phpGPX\Parsers\ExtensionParser; use phpGPX\Parsers\MetadataParser; use phpGPX\Parsers\PointParser; @@ -20,55 +21,24 @@ */ class GpxFile implements \JsonSerializable { - /** - * A list of waypoints. - * @var Point[] - */ - public array $waypoints; + /** @var Point[] */ + public array $waypoints = []; - /** - * A list of routes. - * @var Route[] - */ - public array $routes; + /** @var Route[] */ + public array $routes = []; - /** - * A list of tracks. - * @var Track[] - */ - public array $tracks; + /** @var Track[] */ + public array $tracks = []; - /** - * Metadata about the file. - * The original GPX 1.1 attribute. - * @var Metadata|null - */ - public ?Metadata $metadata; + public ?Metadata $metadata = null; - /** - * @var Extensions|null - */ - public ?Extensions $extensions; - - /** - * Creator of GPX file. - * @var string|null - */ - public ?string $creator; + public ?Extensions $extensions = null; - /** - * GpxFile constructor. - */ - public function __construct() - { - $this->waypoints = []; - $this->routes = []; - $this->tracks = []; - $this->metadata = null; - $this->extensions = null; - $this->creator = null; - } + public ?string $creator = null; + public function __construct( + public readonly Config $config = new Config(), + ) {} public function jsonSerialize(): array { @@ -104,16 +74,14 @@ public function jsonSerialize(): array /** * Return GeoJSON representation of GPX file. - * @return string */ public function toJSON(): string { - return json_encode($this, phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : 0); + return json_encode($this, $this->config->prettyPrint ? JSON_PRETTY_PRINT : 0); } /** * Create XML representation of GPX file. - * @return \DOMDocument */ public function toXML(): \DOMDocument { @@ -170,7 +138,7 @@ public function toXML(): \DOMDocument $document->appendChild($gpx); - if (phpGPX::$PRETTY_PRINT) { + if ($this->config->prettyPrint) { $document->formatOutput = true; $document->preserveWhiteSpace = true; } @@ -179,8 +147,6 @@ public function toXML(): \DOMDocument /** * Save data to file according to selected format. - * @param string $path - * @param string $format */ public function save(string $path, string $format): void { @@ -197,4 +163,4 @@ public function save(string $path, string $format): void throw new \RuntimeException("Unsupported file format!"); } } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index 2c8dbc8..beb6157 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -8,7 +8,6 @@ use phpGPX\Helpers\DateTimeHelper; use phpGPX\Helpers\SerializationHelper; -use phpGPX\phpGPX; enum PointType: string { @@ -247,7 +246,7 @@ public function jsonSerialize(): array $properties = array_filter([ 'name' => $this->name, 'ele' => $this->elevation, - 'time' => DateTimeHelper::formatDateTime($this->time, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), + 'time' => DateTimeHelper::formatDateTime($this->time), 'magvar' => $this->magVar, 'geoidheight' => $this->geoidHeight, 'cmt' => $this->comment, @@ -276,11 +275,4 @@ public function jsonSerialize(): array ]; } - public static function gpxSerialize(\SimpleXMLElement $node): void - { - } - - public function gpxDeserialize(\DOMDocument &$document): void - { - } } diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 50b7d8b..ea7e1fd 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -6,10 +6,10 @@ namespace phpGPX\Models; +use phpGPX\Config; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Helpers\SerializationHelper; -use phpGPX\phpGPX; /** * Class Route @@ -40,17 +40,8 @@ public function __construct() * @return Point[] */ public function getPoints(): array - { - /** @var Point[] $points */ - $points = []; - - $points = array_merge($points, $this->points); - - if (phpGPX::$SORT_BY_TIMESTAMP && !empty($points) && $points[0]->time !== null) { - usort($points, array('phpGPX\Helpers\DateTimeHelper', 'comparePointsByTimestamp')); - } - - return $points; + { + return $this->points; } public function jsonSerialize(): array @@ -86,7 +77,7 @@ public function jsonSerialize(): array * Recalculate stats objects. * @return void */ - public function recalculateStats(): void + public function recalculateStats(Config $config): void { if (empty($this->stats)) { $this->stats = new Stats(); @@ -101,9 +92,9 @@ public function recalculateStats(): void $pointCount = count($this->points); list($this->stats->cumulativeElevationGain, $this->stats->cumulativeElevationLoss) = - ElevationGainLossCalculator::calculate($this->getPoints()); + ElevationGainLossCalculator::calculate($this->getPoints(), $config); - $calculator = new DistanceCalculator($this->getPoints()); + $calculator = new DistanceCalculator($this->getPoints(), $config); $this->stats->distance = $calculator->getRawDistance(); $this->stats->realDistance = $calculator->getRealDistance(); @@ -129,7 +120,7 @@ public function recalculateStats(): void if ($ele === null) { continue; } - if (phpGPX::$IGNORE_ELEVATION_0 && $ele == 0) { + if ($config->ignoreZeroElevation && $ele == 0) { continue; } @@ -157,4 +148,4 @@ public function recalculateStats(): void } } } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index cea63e2..97bbecf 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -6,10 +6,10 @@ namespace phpGPX\Models; +use phpGPX\Config; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Helpers\SerializationHelper; -use phpGPX\phpGPX; /** * Class Segment @@ -82,7 +82,7 @@ public function getPoints(): array * Recalculate stats objects. * @return void */ - public function recalculateStats(): void + public function recalculateStats(Config $config): void { if (empty($this->stats)) { $this->stats = new Stats(); @@ -96,9 +96,9 @@ public function recalculateStats(): void } list($this->stats->cumulativeElevationGain, $this->stats->cumulativeElevationLoss) = - ElevationGainLossCalculator::calculate($this->getPoints()); + ElevationGainLossCalculator::calculate($this->getPoints(), $config); - $calculator = new DistanceCalculator($this->getPoints()); + $calculator = new DistanceCalculator($this->getPoints(), $config); $this->stats->distance = $calculator->getRawDistance(); $this->stats->realDistance = $calculator->getRealDistance(); @@ -124,7 +124,7 @@ public function recalculateStats(): void if ($ele === null) { continue; } - if (phpGPX::$IGNORE_ELEVATION_0 && $ele == 0) { + if ($config->ignoreZeroElevation && $ele == 0) { continue; } @@ -152,4 +152,4 @@ public function recalculateStats(): void } } } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index 2f87c96..12590c3 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -7,7 +7,6 @@ namespace phpGPX\Models; use phpGPX\Helpers\DateTimeHelper; -use phpGPX\phpGPX; /** * Class Stats @@ -142,9 +141,9 @@ public function jsonSerialize(): array 'maxAltitudeCoords' => $this->maxAltitudeCoords, 'cumulativeElevationGain' => $this->cumulativeElevationGain, 'cumulativeElevationLoss' => $this->cumulativeElevationLoss, - 'startedAt' => DateTimeHelper::formatDateTime($this->startedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), + 'startedAt' => DateTimeHelper::formatDateTime($this->startedAt), 'startedAtCoords' => $this->startedAtCoords, - 'finishedAt' => DateTimeHelper::formatDateTime($this->finishedAt, phpGPX::$DATETIME_FORMAT, phpGPX::$DATETIME_TIMEZONE_OUTPUT), + 'finishedAt' => DateTimeHelper::formatDateTime($this->finishedAt), 'finishedAtCoords' => $this->finishedAtCoords, 'duration' => $this->duration, ], fn($v) => $v !== null); diff --git a/src/phpGPX/Models/StatsCalculator.php b/src/phpGPX/Models/StatsCalculator.php index e30e5d1..8a756cb 100644 --- a/src/phpGPX/Models/StatsCalculator.php +++ b/src/phpGPX/Models/StatsCalculator.php @@ -6,18 +6,21 @@ namespace phpGPX\Models; +use phpGPX\Config; + interface StatsCalculator { /** * Recalculate stats objects. + * @param Config $config * @return void */ - public function recalculateStats(): void; + public function recalculateStats(Config $config): void; /** * Return all points in collection. * @return Point[] */ public function getPoints(): array; -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index c21df7f..1c9067f 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -6,8 +6,8 @@ namespace phpGPX\Models; +use phpGPX\Config; use phpGPX\Helpers\SerializationHelper; -use phpGPX\phpGPX; /** * Class Track @@ -37,7 +37,7 @@ public function __construct() * @return Point[] */ public function getPoints(): array - { + { /** @var Point[] $points */ $points = []; @@ -45,10 +45,6 @@ public function getPoints(): array $points = array_merge($points, $segment->points); } - if (phpGPX::$SORT_BY_TIMESTAMP && !empty($points) && $points[0]->time !== null) { - usort($points, array('phpGPX\Helpers\DateTimeHelper', 'comparePointsByTimestamp')); - } - return $points; } @@ -89,7 +85,7 @@ public function jsonSerialize(): array * Recalculate stats objects. * @return void */ - public function recalculateStats(): void + public function recalculateStats(Config $config): void { if (empty($this->stats)) { $this->stats = new Stats(); @@ -104,7 +100,7 @@ public function recalculateStats(): void $segmentsCount = count($this->segments); for ($s = 0; $s < $segmentsCount; $s++) { - $this->segments[$s]->recalculateStats(); + $this->segments[$s]->recalculateStats($config); $segStats = $this->segments[$s]->stats; $this->stats->cumulativeElevationGain += $segStats->cumulativeElevationGain; @@ -145,4 +141,4 @@ public function recalculateStats(): void } } } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/RouteParser.php b/src/phpGPX/Parsers/RouteParser.php index 45349a0..a13bc90 100644 --- a/src/phpGPX/Parsers/RouteParser.php +++ b/src/phpGPX/Parsers/RouteParser.php @@ -7,7 +7,6 @@ namespace phpGPX\Parsers; use phpGPX\Models\Route; -use phpGPX\phpGPX; /** * Class RouteParser @@ -81,10 +80,6 @@ public static function parse(\SimpleXMLElement $nodes): array } } - if (phpGPX::$CALCULATE_STATS) { - $route->recalculateStats(); - } - $routes[] = $route; } diff --git a/src/phpGPX/Parsers/SegmentParser.php b/src/phpGPX/Parsers/SegmentParser.php index ff22afa..18bb5e8 100644 --- a/src/phpGPX/Parsers/SegmentParser.php +++ b/src/phpGPX/Parsers/SegmentParser.php @@ -7,7 +7,6 @@ namespace phpGPX\Parsers; use phpGPX\Models\Segment; -use phpGPX\phpGPX; /** * Class SegmentParser @@ -42,10 +41,6 @@ public static function parse(\SimpleXMLElement $node): ?Segment $segment->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null; - if (phpGPX::$CALCULATE_STATS) { - $segment->recalculateStats(); - } - return $segment; } diff --git a/src/phpGPX/Parsers/TrackParser.php b/src/phpGPX/Parsers/TrackParser.php index ccf47a4..0a455be 100644 --- a/src/phpGPX/Parsers/TrackParser.php +++ b/src/phpGPX/Parsers/TrackParser.php @@ -7,7 +7,6 @@ namespace phpGPX\Parsers; use phpGPX\Models\Track; -use phpGPX\phpGPX; /** * Class TrackParser @@ -81,10 +80,6 @@ public static function parse(\SimpleXMLElement $nodes): array } } - if (phpGPX::$CALCULATE_STATS) { - $track->recalculateStats(); - } - $tracks[] = $track; } diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index 4bb505e..d5368db 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -6,6 +6,7 @@ namespace phpGPX; +use phpGPX\Helpers\DateTimeHelper; use phpGPX\Models\GpxFile; use phpGPX\Parsers\MetadataParser; use phpGPX\Parsers\RouteParser; @@ -18,126 +19,84 @@ */ class phpGPX { - const JSON_FORMAT = 'json'; + const JSON_FORMAT = 'json'; const XML_FORMAT = 'xml'; const GEOJSON_FORMAT = 'geojson'; const PACKAGE_NAME = 'phpGPX'; - const VERSION = '2.0.0-alpha.1'; + const VERSION = '2.0.0-alpha.2'; - /** - * Pretty print XML output - * @var bool - */ - public static bool $PRETTY_PRINT = true; - - /** - * Ignore elevation values of 0 - * @var bool - */ - public static bool $IGNORE_ELEVATION_0 = false; - - /** - * Calculate stats for tracks, segments and routes - * @var bool - */ - public static bool $CALCULATE_STATS = true; - - /** - * DateTime format for output - * @var string - */ - public static string $DATETIME_FORMAT = 'c'; + public readonly Config $config; - /** - * DateTime timezone output - * @var string|null - */ - public static ?string $DATETIME_TIMEZONE_OUTPUT = null; - - /** - * Additional sort based on timestamp in Routes & Tracks on XML read. - * @var bool - */ - public static bool $SORT_BY_TIMESTAMP = false; - - /** - * Apply elevation gain/loss smoothing - * @var bool - */ - public static bool $APPLY_ELEVATION_SMOOTHING = false; - - /** - * Minimum elevation difference threshold in meters for smoothing - * @var int - */ - public static int $ELEVATION_SMOOTHING_THRESHOLD = 2; - - /** - * Maximum elevation difference threshold in meters for spike filtering - * @var int|null - */ - public static ?int $ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; - - /** - * Apply distance calculation smoothing - * @var bool - */ - public static bool $APPLY_DISTANCE_SMOOTHING = false; - - /** - * Minimum distance threshold in meters for smoothing - * @var int - */ - public static int $DISTANCE_SMOOTHING_THRESHOLD = 2; + public function __construct(?Config $config = null) + { + $this->config = $config ?? new Config(); + } /** - * Load GPX file. - * @param string $path - * @return GpxFile + * Load GPX file from path. */ - public static function load(string $path): GpxFile + public function load(string $path): GpxFile { - $xml = file_get_contents($path); - - return self::parse($xml); + return $this->parse(file_get_contents($path)); } /** * Parse GPX data string. - * @param string $xml - * @return GpxFile */ - public static function parse(string $xml): GpxFile + public function parse(string $xml): GpxFile { - $xml = simplexml_load_string($xml); - - $gpx = new GpxFile(); - - // Parse creator - $gpx->creator = isset($xml['creator']) ? (string)$xml['creator'] : null; + $xmlElement = simplexml_load_string($xml); - // Parse metadata - $gpx->metadata = isset($xml->metadata) ? MetadataParser::parse($xml->metadata) : null; + $gpx = new GpxFile($this->config); - // Parse waypoints - $gpx->waypoints = isset($xml->wpt) ? WaypointParser::parse($xml->wpt) : []; + $gpx->creator = isset($xmlElement['creator']) ? (string)$xmlElement['creator'] : null; + $gpx->metadata = isset($xmlElement->metadata) ? MetadataParser::parse($xmlElement->metadata) : null; + $gpx->waypoints = isset($xmlElement->wpt) ? WaypointParser::parse($xmlElement->wpt) : []; + $gpx->tracks = isset($xmlElement->trk) ? TrackParser::parse($xmlElement->trk) : []; + $gpx->routes = isset($xmlElement->rte) ? RouteParser::parse($xmlElement->rte) : []; - // Parse tracks - $gpx->tracks = isset($xml->trk) ? TrackParser::parse($xml->trk) : []; + if ($this->config->sortByTimestamp) { + $this->sortPointsByTimestamp($gpx); + } - // Parse routes - $gpx->routes = isset($xml->rte) ? RouteParser::parse($xml->rte) : []; + if ($this->config->calculateStats) { + foreach ($gpx->tracks as $track) { + $track->recalculateStats($this->config); + } + foreach ($gpx->routes as $route) { + $route->recalculateStats($this->config); + } + } return $gpx; } + /** + * Sort all point arrays in-place by timestamp. + */ + private function sortPointsByTimestamp(GpxFile $gpx): void + { + foreach ($gpx->tracks as $track) { + foreach ($track->segments as $segment) { + if (!empty($segment->points) && $segment->points[0]->time !== null) { + usort($segment->points, [DateTimeHelper::class, 'comparePointsByTimestamp']); + } + } + } + + foreach ($gpx->routes as $route) { + if (!empty($route->points) && $route->points[0]->time !== null) { + usort($route->points, [DateTimeHelper::class, 'comparePointsByTimestamp']); + } + } + } + /** * Create library signature from name and version. - * @return string */ public static function getSignature(): string { return sprintf("%s/%s", self::PACKAGE_NAME, self::VERSION); } -} +} \ No newline at end of file diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php index 8cc5f92..71d088b 100644 --- a/tests/Integration/GeoJsonOutputTest.php +++ b/tests/Integration/GeoJsonOutputTest.php @@ -2,6 +2,7 @@ namespace phpGPX\Tests\Integration; +use phpGPX\Config; use phpGPX\Models\Point; use phpGPX\Models\Route; use phpGPX\Models\Segment; @@ -13,9 +14,16 @@ class GeoJsonOutputTest extends TestCase { private const FIXTURES_DIR = __DIR__ . '/../Fixtures'; + private phpGPX $gpx; + + protected function setUp(): void + { + $this->gpx = new phpGPX(); + } + public function testGpxFileJsonSerializeIsFeatureCollection(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); $json = $gpxFile->jsonSerialize(); $this->assertEquals('FeatureCollection', $json['type']); @@ -58,7 +66,7 @@ public function testRouteJsonIsLineStringFeature(): void $p2->elevation = 1.0; $route->points = [$p1, $p2]; - $route->recalculateStats(); + $route->recalculateStats(new Config()); $json = $route->jsonSerialize(); @@ -97,7 +105,7 @@ public function testTrackJsonIsMultiLineStringFeature(): void $seg2->points = [$p3]; $track->segments = [$seg1, $seg2]; - $track->recalculateStats(); + $track->recalculateStats(new Config()); $json = $track->jsonSerialize(); @@ -111,7 +119,7 @@ public function testTrackJsonIsMultiLineStringFeature(): void public function testLoadedFileGeoJsonStructure(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/minimal.gpx'); $json = json_decode(json_encode($gpxFile), true); $this->assertEquals('FeatureCollection', $json['type']); @@ -132,7 +140,7 @@ public function testLoadedFileGeoJsonStructure(): void public function testGeoJsonWithWaypoints(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/timezero.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/timezero.gpx'); $json = json_decode(json_encode($gpxFile), true); $this->assertEquals('FeatureCollection', $json['type']); @@ -151,7 +159,7 @@ public function testGeoJsonWithWaypoints(): void public function testToJsonOutput(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); $geoJson = $gpxFile->toJSON(); $decoded = json_decode($geoJson, true); diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php index edff0ae..f755d33 100644 --- a/tests/Integration/GpxFileLoadTest.php +++ b/tests/Integration/GpxFileLoadTest.php @@ -9,9 +9,16 @@ class GpxFileLoadTest extends TestCase { private const FIXTURES_DIR = __DIR__ . '/../Fixtures'; + private phpGPX $gpx; + + protected function setUp(): void + { + $this->gpx = new phpGPX(); + } + public function testLoadTimezeroGpx(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/timezero.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/timezero.gpx'); // Waypoints $this->assertCount(2, $gpxFile->waypoints); @@ -49,7 +56,7 @@ public function testLoadTimezeroGpx(): void public function testLoadRouteGpx(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); $this->assertEmpty($gpxFile->tracks); $this->assertEmpty($gpxFile->waypoints); @@ -77,7 +84,7 @@ public function testLoadRouteGpx(): void public function testLoadGpsTrackGpx(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/gps-track.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/gps-track.gpx'); $this->assertCount(1, $gpxFile->tracks); $this->assertEquals('GPS-Track', $gpxFile->tracks[0]->name); @@ -102,7 +109,7 @@ public function testLoadGpsTrackGpx(): void public function testLoadMinimalGpx(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/minimal.gpx'); // Has metadata $this->assertNotNull($gpxFile->metadata); @@ -130,14 +137,14 @@ public function testLoadMinimalGpx(): void public function testLoadCreatorAttribute(): void { - $gpxFile = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); $this->assertEquals('RouteConverter', $gpxFile->creator); } public function testParseFromString(): void { $xml = file_get_contents(self::FIXTURES_DIR . '/route.gpx'); - $gpxFile = phpGPX::parse($xml); + $gpxFile = $this->gpx->parse($xml); $this->assertCount(2, $gpxFile->routes); $this->assertEquals("Patrick's Route", $gpxFile->routes[0]->name); diff --git a/tests/Integration/XmlRoundTripTest.php b/tests/Integration/XmlRoundTripTest.php index 9b78efc..38d50f9 100644 --- a/tests/Integration/XmlRoundTripTest.php +++ b/tests/Integration/XmlRoundTripTest.php @@ -9,14 +9,21 @@ class XmlRoundTripTest extends TestCase { private const FIXTURES_DIR = __DIR__ . '/../Fixtures'; + private phpGPX $gpx; + + protected function setUp(): void + { + $this->gpx = new phpGPX(); + } + /** * Load a GPX file, serialize to XML, parse again, and verify key data is preserved. */ public function testRoundTripTimezero(): void { - $original = phpGPX::load(self::FIXTURES_DIR . '/timezero.gpx'); + $original = $this->gpx->load(self::FIXTURES_DIR . '/timezero.gpx'); $xml = $original->toXML()->saveXML(); - $reloaded = phpGPX::parse($xml); + $reloaded = $this->gpx->parse($xml); $this->assertCount(count($original->waypoints), $reloaded->waypoints); $this->assertCount(count($original->tracks), $reloaded->tracks); @@ -50,9 +57,9 @@ public function testRoundTripTimezero(): void public function testRoundTripRoute(): void { - $original = phpGPX::load(self::FIXTURES_DIR . '/route.gpx'); + $original = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); $xml = $original->toXML()->saveXML(); - $reloaded = phpGPX::parse($xml); + $reloaded = $this->gpx->parse($xml); $this->assertCount(count($original->routes), $reloaded->routes); @@ -76,9 +83,9 @@ public function testRoundTripRoute(): void public function testRoundTripGpsTrack(): void { - $original = phpGPX::load(self::FIXTURES_DIR . '/gps-track.gpx'); + $original = $this->gpx->load(self::FIXTURES_DIR . '/gps-track.gpx'); $xml = $original->toXML()->saveXML(); - $reloaded = phpGPX::parse($xml); + $reloaded = $this->gpx->parse($xml); $this->assertCount(1, $reloaded->tracks); $this->assertEquals('GPS-Track', $reloaded->tracks[0]->name); @@ -100,9 +107,9 @@ public function testRoundTripGpsTrack(): void public function testRoundTripMinimalWithExtensions(): void { - $original = phpGPX::load(self::FIXTURES_DIR . '/minimal.gpx'); + $original = $this->gpx->load(self::FIXTURES_DIR . '/minimal.gpx'); $xml = $original->toXML()->saveXML(); - $reloaded = phpGPX::parse($xml); + $reloaded = $this->gpx->parse($xml); // Metadata survives $this->assertNotNull($reloaded->metadata); @@ -127,9 +134,9 @@ public function testRoundTripMinimalWithExtensions(): void public function testRoundTripStatsConsistency(): void { - $original = phpGPX::load(self::FIXTURES_DIR . '/gps-track.gpx'); + $original = $this->gpx->load(self::FIXTURES_DIR . '/gps-track.gpx'); $xml = $original->toXML()->saveXML(); - $reloaded = phpGPX::parse($xml); + $reloaded = $this->gpx->parse($xml); $origStats = $original->tracks[0]->stats; $reloadedStats = $reloaded->tracks[0]->stats; diff --git a/tests/Unit/Helpers/DistanceCalculatorTest.php b/tests/Unit/Helpers/DistanceCalculatorTest.php index 7acf21c..7235ff3 100644 --- a/tests/Unit/Helpers/DistanceCalculatorTest.php +++ b/tests/Unit/Helpers/DistanceCalculatorTest.php @@ -2,19 +2,14 @@ namespace phpGPX\Tests\Unit\Helpers; +use phpGPX\Config; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; -use phpGPX\phpGPX; use PHPUnit\Framework\TestCase; class DistanceCalculatorTest extends TestCase { - protected function setUp(): void - { - phpGPX::$APPLY_DISTANCE_SMOOTHING = false; - } - private function makePoint(float $lat, float $lon, ?float $ele = null): Point { $p = new Point(Point::TRACKPOINT); @@ -26,7 +21,7 @@ private function makePoint(float $lat, float $lon, ?float $ele = null): Point public function testEmptyPoints(): void { - $calc = new DistanceCalculator([]); + $calc = new DistanceCalculator([], new Config()); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); $this->assertEqualsWithDelta(0.0, $calc->getRealDistance(), 0.001); } @@ -34,7 +29,7 @@ public function testEmptyPoints(): void public function testSinglePoint(): void { $points = [$this->makePoint(48.157, 17.054)]; - $calc = new DistanceCalculator($points); + $calc = new DistanceCalculator($points, new Config()); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); } @@ -46,7 +41,7 @@ public function testTwoPoints(): void $expectedRaw = GeoHelper::getRawDistance($p1, $p2); $expectedReal = GeoHelper::getRealDistance($p1, $p2); - $calc = new DistanceCalculator([$p1, $p2]); + $calc = new DistanceCalculator([$p1, $p2], new Config()); $this->assertEqualsWithDelta($expectedRaw, $calc->getRawDistance(), 0.01); $this->assertEqualsWithDelta($expectedReal, $calc->getRealDistance(), 0.01); @@ -62,7 +57,7 @@ public function testMultiplePointsAccumulate(): void $d12 = GeoHelper::getRawDistance($p1, $p2); $d23 = GeoHelper::getRawDistance($p2, $p3); - $calc = new DistanceCalculator([$p1, $p2, $p3]); + $calc = new DistanceCalculator([$p1, $p2, $p3], new Config()); $totalRaw = $calc->getRawDistance(); $this->assertEqualsWithDelta($d12 + $d23, $totalRaw, 0.01); @@ -74,7 +69,7 @@ public function testPointsDifferenceAndDistanceAreSet(): void $p2 = $this->makePoint(46.572016, 8.414866); $p3 = $this->makePoint(46.572088, 8.414911); - $calc = new DistanceCalculator([$p1, $p2, $p3]); + $calc = new DistanceCalculator([$p1, $p2, $p3], new Config()); $calc->getRawDistance(); // First point should have no difference set @@ -91,38 +86,38 @@ public function testPointsDifferenceAndDistanceAreSet(): void public function testDistanceSmoothingFiltersSmallMovements(): void { - phpGPX::$APPLY_DISTANCE_SMOOTHING = true; - phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 10; // 10 meter threshold + $config = new Config( + applyDistanceSmoothing: true, + distanceSmoothingThreshold: 10, + ); // Points very close together (< 10m apart) $p1 = $this->makePoint(46.571948, 8.414757); $p2 = $this->makePoint(46.571949, 8.414758); // ~0.1m away $p3 = $this->makePoint(46.571950, 8.414759); // ~0.1m away - $calc = new DistanceCalculator([$p1, $p2, $p3]); + $calc = new DistanceCalculator([$p1, $p2, $p3], $config); $distance = $calc->getRawDistance(); // With smoothing, these tiny movements should be filtered out $this->assertEqualsWithDelta(0.0, $distance, 0.01); - - phpGPX::$APPLY_DISTANCE_SMOOTHING = false; } public function testDistanceSmoothingKeepsLargeMovements(): void { - phpGPX::$APPLY_DISTANCE_SMOOTHING = true; - phpGPX::$DISTANCE_SMOOTHING_THRESHOLD = 2; + $config = new Config( + applyDistanceSmoothing: true, + distanceSmoothingThreshold: 2, + ); // Points ~857m apart — well above threshold $p1 = $this->makePoint(48.1573923225717, 17.0547121910204); $p2 = $this->makePoint(48.1644916381763, 17.0591753907502); - $calc = new DistanceCalculator([$p1, $p2]); + $calc = new DistanceCalculator([$p1, $p2], $config); $distance = $calc->getRawDistance(); $this->assertGreaterThan(800, $distance); - - phpGPX::$APPLY_DISTANCE_SMOOTHING = false; } public function testSamePointRepeatedZeroDistance(): void @@ -131,7 +126,7 @@ public function testSamePointRepeatedZeroDistance(): void $p2 = $this->makePoint(46.571948, 8.414757); $p3 = $this->makePoint(46.571948, 8.414757); - $calc = new DistanceCalculator([$p1, $p2, $p3]); + $calc = new DistanceCalculator([$p1, $p2, $p3], new Config()); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); } } \ No newline at end of file diff --git a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php index 54eb1ab..4afc9d7 100644 --- a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php +++ b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php @@ -2,21 +2,13 @@ namespace phpGPX\Tests\Unit\Helpers; +use phpGPX\Config; use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Models\Point; -use phpGPX\phpGPX; use PHPUnit\Framework\TestCase; class ElevationGainLossCalculatorTest extends TestCase { - protected function setUp(): void - { - phpGPX::$APPLY_ELEVATION_SMOOTHING = false; - phpGPX::$IGNORE_ELEVATION_0 = false; - phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; - phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; - } - private function makePoint(float $ele): Point { $p = new Point(Point::TRACKPOINT); @@ -28,14 +20,14 @@ private function makePoint(float $ele): Point public function testEmptyPoints(): void { - [$gain, $loss] = ElevationGainLossCalculator::calculate([]); + [$gain, $loss] = ElevationGainLossCalculator::calculate([], new Config()); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testSinglePoint(): void { - [$gain, $loss] = ElevationGainLossCalculator::calculate([$this->makePoint(100)]); + [$gain, $loss] = ElevationGainLossCalculator::calculate([$this->makePoint(100)], new Config()); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } @@ -48,7 +40,7 @@ public function testFlatTrack(): void $this->makePoint(100), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } @@ -61,7 +53,7 @@ public function testUphillOnly(): void $this->makePoint(200), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); $this->assertEqualsWithDelta(100.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } @@ -74,7 +66,7 @@ public function testDownhillOnly(): void $this->makePoint(100), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(100.0, $loss, 0.001); } @@ -89,7 +81,7 @@ public function testUpAndDown(): void $this->makePoint(140), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); $this->assertEqualsWithDelta(70.0, $gain, 0.001); // 50 + 20 $this->assertEqualsWithDelta(30.0, $loss, 0.001); } @@ -103,14 +95,14 @@ public function testNullElevationSkipped(): void $p2->elevation = null; $p3 = $this->makePoint(200); - [$gain, $loss] = ElevationGainLossCalculator::calculate([$p1, $p2, $p3]); + [$gain, $loss] = ElevationGainLossCalculator::calculate([$p1, $p2, $p3], new Config()); $this->assertEqualsWithDelta(100.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testIgnoreElevationZero(): void { - phpGPX::$IGNORE_ELEVATION_0 = true; + $config = new Config(ignoreZeroElevation: true); $points = [ $this->makePoint(100), @@ -118,14 +110,14 @@ public function testIgnoreElevationZero(): void $this->makePoint(200), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); $this->assertEqualsWithDelta(100.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testIgnoreElevationZeroDisabled(): void { - phpGPX::$IGNORE_ELEVATION_0 = false; + $config = new Config(ignoreZeroElevation: false); $points = [ $this->makePoint(100), @@ -133,15 +125,17 @@ public function testIgnoreElevationZeroDisabled(): void $this->makePoint(200), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); $this->assertEqualsWithDelta(200.0, $gain, 0.001); // 0→200 $this->assertEqualsWithDelta(100.0, $loss, 0.001); // 100→0 } public function testSmoothingFiltersSmallChanges(): void { - phpGPX::$APPLY_ELEVATION_SMOOTHING = true; - phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 5; + $config = new Config( + applyElevationSmoothing: true, + elevationSmoothingThreshold: 5, + ); // Small oscillations of 2m — below 5m threshold, should be filtered $points = [ @@ -152,15 +146,17 @@ public function testSmoothingFiltersSmallChanges(): void $this->makePoint(100), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testSmoothingKeepsLargeChanges(): void { - phpGPX::$APPLY_ELEVATION_SMOOTHING = true; - phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 5; + $config = new Config( + applyElevationSmoothing: true, + elevationSmoothingThreshold: 5, + ); // Large change of 50m — above 5m threshold $points = [ @@ -168,16 +164,18 @@ public function testSmoothingKeepsLargeChanges(): void $this->makePoint(150), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); $this->assertEqualsWithDelta(50.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testSmoothingSpikesThreshold(): void { - phpGPX::$APPLY_ELEVATION_SMOOTHING = true; - phpGPX::$ELEVATION_SMOOTHING_THRESHOLD = 2; - phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD = 50; + $config = new Config( + applyElevationSmoothing: true, + elevationSmoothingThreshold: 2, + elevationSmoothingSpikesThreshold: 50, + ); // Spike of 100m — above spikes threshold, should be filtered $points = [ @@ -186,7 +184,7 @@ public function testSmoothingSpikesThreshold(): void $this->makePoint(105), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); // The 100m jump is filtered (> spikes threshold) // The 200→105 drop: delta from last considered (100) to 200 is 100 (filtered) // delta from 100 to 105 is 5 (above 2, below 50) — counted diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php index bc83cfc..653743c 100644 --- a/tests/Unit/Models/StatsCalculationTest.php +++ b/tests/Unit/Models/StatsCalculationTest.php @@ -2,22 +2,21 @@ namespace phpGPX\Tests\Unit\Models; +use phpGPX\Config; use phpGPX\Models\Point; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Stats; use phpGPX\Models\Track; -use phpGPX\phpGPX; use PHPUnit\Framework\TestCase; class StatsCalculationTest extends TestCase { + private Config $config; + protected function setUp(): void { - phpGPX::$CALCULATE_STATS = true; - phpGPX::$IGNORE_ELEVATION_0 = false; - phpGPX::$APPLY_DISTANCE_SMOOTHING = false; - phpGPX::$APPLY_ELEVATION_SMOOTHING = false; + $this->config = new Config(); } private function makePoint( @@ -53,7 +52,7 @@ private function makeRoutePoint( public function testSegmentStatsEmptyPoints(): void { $segment = new Segment(); - $segment->recalculateStats(); + $segment->recalculateStats($this->config); $this->assertInstanceOf(Stats::class, $segment->stats); $this->assertNull($segment->stats->distance); @@ -65,7 +64,7 @@ public function testSegmentStatsSinglePoint(): void $segment->points = [ $this->makePoint(46.571948, 8.414757, 2419, '2017-08-13T07:10:41Z'), ]; - $segment->recalculateStats(); + $segment->recalculateStats($this->config); $this->assertEqualsWithDelta(0.0, $segment->stats->distance, 0.01); $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationGain, 0.01); @@ -84,7 +83,7 @@ public function testSegmentStatsBasicTrack(): void $this->makePoint(46.572069, 8.414912, 2422, '2017-08-13T07:12:15Z'), $this->makePoint(46.572054, 8.414888, 2425, '2017-08-13T07:12:18Z'), ]; - $segment->recalculateStats(); + $segment->recalculateStats($this->config); // Distance should be positive $this->assertGreaterThan(0, $segment->stats->distance); @@ -121,7 +120,7 @@ public function testSegmentStatsWithoutTimestamps(): void $this->makePoint(46.571948, 8.414757, 100), $this->makePoint(46.572016, 8.414866, 200), ]; - $segment->recalculateStats(); + $segment->recalculateStats($this->config); // Distance should still be calculated $this->assertGreaterThan(0, $segment->stats->distance); @@ -139,7 +138,7 @@ public function testSegmentStatsWithoutElevation(): void $this->makePoint(46.571948, 8.414757, null, '2017-08-13T07:10:41Z'), $this->makePoint(46.572016, 8.414866, null, '2017-08-13T07:10:54Z'), ]; - $segment->recalculateStats(); + $segment->recalculateStats($this->config); $this->assertGreaterThan(0, $segment->stats->distance); $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationGain, 0.001); @@ -148,7 +147,7 @@ public function testSegmentStatsWithoutElevation(): void public function testSegmentStatsIgnoreElevationZero(): void { - phpGPX::$IGNORE_ELEVATION_0 = true; + $config = new Config(ignoreZeroElevation: true); $segment = new Segment(); $segment->points = [ @@ -156,9 +155,9 @@ public function testSegmentStatsIgnoreElevationZero(): void $this->makePoint(46.572016, 8.414866, 0, '2017-08-13T07:10:54Z'), $this->makePoint(46.572088, 8.414911, 200, '2017-08-13T07:11:56Z'), ]; - $segment->recalculateStats(); + $segment->recalculateStats($config); - // minAltitude should NOT be 0 when IGNORE_ELEVATION_0 is true + // minAltitude should NOT be 0 when ignoreZeroElevation is true $this->assertGreaterThan(0, $segment->stats->minAltitude); } @@ -169,11 +168,11 @@ public function testSegmentStatsRecalculateResetsValues(): void $this->makePoint(46.571948, 8.414757, 100, '2017-08-13T07:10:41Z'), $this->makePoint(46.572016, 8.414866, 200, '2017-08-13T07:10:54Z'), ]; - $segment->recalculateStats(); + $segment->recalculateStats($this->config); $firstDistance = $segment->stats->distance; // Recalculate again — should get same result (not accumulated) - $segment->recalculateStats(); + $segment->recalculateStats($this->config); $this->assertEqualsWithDelta($firstDistance, $segment->stats->distance, 0.001); } @@ -182,7 +181,7 @@ public function testSegmentStatsRecalculateResetsValues(): void public function testTrackStatsEmptySegments(): void { $track = new Track(); - $track->recalculateStats(); + $track->recalculateStats($this->config); $this->assertInstanceOf(Stats::class, $track->stats); $this->assertNull($track->stats->distance); @@ -198,7 +197,7 @@ public function testTrackStatsSingleSegment(): void $track = new Track(); $track->segments = [$segment]; - $track->recalculateStats(); + $track->recalculateStats($this->config); $this->assertGreaterThan(0, $track->stats->distance); $this->assertEqualsWithDelta(6.0, $track->stats->cumulativeElevationGain, 0.01); @@ -222,11 +221,11 @@ public function testTrackStatsMultipleSegmentsAggregated(): void $track = new Track(); $track->segments = [$seg1, $seg2]; - $track->recalculateStats(); + $track->recalculateStats($this->config); // Distances should be summed across segments - $seg1->recalculateStats(); - $seg2->recalculateStats(); + $seg1->recalculateStats($this->config); + $seg2->recalculateStats($this->config); $expectedDistance = $seg1->stats->distance + $seg2->stats->distance; $this->assertEqualsWithDelta($expectedDistance, $track->stats->distance, 0.01); @@ -275,7 +274,7 @@ public function testTrackGetPointsFlattensSegments(): void public function testRouteStatsEmptyPoints(): void { $route = new Route(); - $route->recalculateStats(); + $route->recalculateStats($this->config); $this->assertInstanceOf(Stats::class, $route->stats); $this->assertNull($route->stats->distance); @@ -290,7 +289,7 @@ public function testRouteStatsBasic(): void $this->makeRoutePoint(54.93327743521187, 9.86187816543752, 2.0), $this->makeRoutePoint(54.93342326167919, 9.862439849679859, 3.0), ]; - $route->recalculateStats(); + $route->recalculateStats($this->config); $this->assertGreaterThan(0, $route->stats->distance); $this->assertEqualsWithDelta(3.0, $route->stats->cumulativeElevationGain, 0.01); From 5161aef3e1a4b69d8ec4ce57412fb6e730122b46 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 22:04:18 +0100 Subject: [PATCH 19/31] =?UTF-8?q?Computing=20Engine=20(saying=20goodbye=20?= =?UTF-8?q?to=20failed=20Midleware=20idea)=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 178 ++++------ docs/00_Getting_Started/02_Quick_Start.md | 10 +- docs/01_Usage/01_Loading_Files.md | 58 ++-- docs/01_Usage/02_Creating_Files.md | 23 +- docs/01_Usage/03_Statistics.md | 178 +++++++--- docs/01_Usage/04_Configuration.md | 115 +++++-- src/phpGPX/Analysis/AbstractPointAnalyzer.php | 58 ++++ src/phpGPX/Analysis/AltitudeAnalyzer.php | 102 ++++++ src/phpGPX/Analysis/BoundsAnalyzer.php | 178 ++++++++++ src/phpGPX/Analysis/DistanceAnalyzer.php | 106 ++++++ src/phpGPX/Analysis/ElevationAnalyzer.php | 118 +++++++ src/phpGPX/Analysis/Engine.php | 307 +++++++++++++++++ src/phpGPX/Analysis/MovementAnalyzer.php | 99 ++++++ .../Analysis/PointAnalyzerInterface.php | 90 +++++ src/phpGPX/Analysis/TimestampAnalyzer.php | 91 +++++ .../Analysis/TrackPointExtensionAnalyzer.php | 153 +++++++++ src/phpGPX/Config.php | 24 -- src/phpGPX/Helpers/DateTimeHelper.php | 16 - src/phpGPX/Helpers/DistanceCalculator.php | 21 +- .../Helpers/ElevationGainLossCalculator.php | 21 +- src/phpGPX/Models/Collection.php | 3 +- src/phpGPX/Models/Route.php | 79 ----- src/phpGPX/Models/Segment.php | 82 +---- src/phpGPX/Models/Stats.php | 56 +++ src/phpGPX/Models/StatsCalculator.php | 26 -- src/phpGPX/Models/Track.php | 62 ---- src/phpGPX/phpGPX.php | 51 +-- tests/Integration/GeoJsonOutputTest.php | 3 - tests/Integration/GpxFileLoadTest.php | 3 +- tests/Integration/XmlRoundTripTest.php | 3 +- tests/Unit/Analysis/BoundsAnalyzerTest.php | 166 +++++++++ tests/Unit/Analysis/EngineTest.php | 323 ++++++++++++++++++ tests/Unit/Analysis/MovementAnalyzerTest.php | 178 ++++++++++ .../TrackPointExtensionAnalyzerTest.php | 174 ++++++++++ tests/Unit/Helpers/DateTimeHelperTest.php | 14 - tests/Unit/Helpers/DistanceCalculatorTest.php | 27 +- .../ElevationGainLossCalculatorTest.php | 58 ++-- tests/Unit/Models/StatsCalculationTest.php | 197 +++++++---- 38 files changed, 2747 insertions(+), 704 deletions(-) create mode 100644 src/phpGPX/Analysis/AbstractPointAnalyzer.php create mode 100644 src/phpGPX/Analysis/AltitudeAnalyzer.php create mode 100644 src/phpGPX/Analysis/BoundsAnalyzer.php create mode 100644 src/phpGPX/Analysis/DistanceAnalyzer.php create mode 100644 src/phpGPX/Analysis/ElevationAnalyzer.php create mode 100644 src/phpGPX/Analysis/Engine.php create mode 100644 src/phpGPX/Analysis/MovementAnalyzer.php create mode 100644 src/phpGPX/Analysis/PointAnalyzerInterface.php create mode 100644 src/phpGPX/Analysis/TimestampAnalyzer.php create mode 100644 src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php delete mode 100644 src/phpGPX/Models/StatsCalculator.php create mode 100644 tests/Unit/Analysis/BoundsAnalyzerTest.php create mode 100644 tests/Unit/Analysis/EngineTest.php create mode 100644 tests/Unit/Analysis/MovementAnalyzerTest.php create mode 100644 tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php diff --git a/README.md b/README.md index 7b283f3..9c32f4f 100644 --- a/README.md +++ b/README.md @@ -17,34 +17,35 @@ Repository branches: ## Features - Full support of [official specification](http://www.topografix.com/GPX/1/1/). - - Statistics calculation. - - Extensions. - - JSON & XML & PHP Array output. + - Single-pass stats engine with pluggable analyzers. + - Extensions support. + - JSON (GeoJSON) & XML output. ### Supported Extensions -- Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd): +- Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd): http://www.garmin.com/xmlschemas/TrackPointExtension/v1 ### Stats calculation -- (Smoothed) Distance (m) -- Average speed (m/s) -- Average pace (s/km) -- Min / max altitude (m) -- Min / max coordinates ([lat,lng]) -- (Smoothed) Elevation gain / loss (m) -- Start / end (DateTime object) -- Start / end coordinates ([lat,lng]) -- Duration (seconds) +Stats are provided by the `Engine` and its analyzers: + +- (Smoothed) Distance (m) — `DistanceAnalyzer` +- Average speed (m/s), average pace (s/km) — derived by engine +- Min / max altitude with coordinates — `AltitudeAnalyzer` +- (Smoothed) Elevation gain / loss (m) — `ElevationAnalyzer` +- Start / end timestamps with coordinates — `TimestampAnalyzer` +- Duration (seconds) — derived by engine +- Coordinate bounds (min/max lat/lon) — `BoundsAnalyzer` +- Moving duration and moving average speed — `MovementAnalyzer` +- Heart rate, cadence, temperature — `TrackPointExtensionAnalyzer` ## Installation -You can easily install phpGPX library with [composer](https://getcomposer.org/). There is no stable release yet, so -please use release candidates. +You can easily install phpGPX library with [composer](https://getcomposer.org/). ``` -composer require sibyx/phpgpx:1.3.0 +composer require sibyx/phpgpx ``` ## Examples @@ -54,20 +55,20 @@ composer require sibyx/phpgpx:1.3.0 ```php load('example.gpx'); - -foreach ($file->tracks as $track) -{ + +foreach ($file->tracks as $track) { // Statistics for whole track - $track->stats->toArray(); - - foreach ($track->segments as $segment) - { - // Statistics for segment of track - $segment->stats->toArray(); + echo "Distance: " . round($track->stats->distance) . " m\n"; + echo "Duration: " . gmdate("H:i:s", $track->stats->duration) . "\n"; + + foreach ($track->segments as $segment) { + // Statistics for segment of track + echo " Segment distance: " . round($segment->stats->distance) . " m\n"; } } ``` @@ -78,13 +79,13 @@ foreach ($file->tracks as $track) use phpGPX\phpGPX; $gpx = new phpGPX(); - + $file = $gpx->load('example.gpx'); // XML $file->save('output.gpx', phpGPX::XML_FORMAT); - -//JSON + +// JSON (GeoJSON) $file->save('output.json', phpGPX::JSON_FORMAT); ``` @@ -164,9 +165,7 @@ $track->source = "MySpecificGarminDevice"; // Creating Track segment $segment = new Segment(); - -foreach ($sample_data as $sample_point) -{ +foreach ($sample_data as $sample_point) { // Creating trackpoint $point = new Point(Point::TRACKPOINT); $point->latitude = $sample_point['latitude']; @@ -180,20 +179,16 @@ foreach ($sample_data as $sample_point) // Add segment to segment array of track $track->segments[] = $segment; -// Recalculate stats based on received data -$track->recalculateStats(); - // Add track to file $gpx_file->tracks[] = $track; // GPX output $gpx_file->save('CreatingFileFromScratchExample.gpx', \phpGPX\phpGPX::XML_FORMAT); -// Serialized data as JSON +// Serialized data as JSON (GeoJSON) $gpx_file->save('CreatingFileFromScratchExample.json', \phpGPX\phpGPX::JSON_FORMAT); // Direct GPX output to browser - header("Content-Type: application/gpx+xml"); header("Content-Disposition: attachment; filename=CreatingFileFromScratchExample.gpx"); @@ -201,83 +196,56 @@ echo $gpx_file->toXML()->saveXML(); exit(); ``` -Currently, supported output formats: +Currently supported output formats: - XML - - JSON + - JSON (GeoJSON, RFC 7946) ## Configuration -Use the static constants in phpGPX to modify behaviour. +Output formatting is configured via the `Config` value object. Stats computation is configured via analyzer constructor arguments. + +```php +use phpGPX\phpGPX; +use phpGPX\Config; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX( + config: new Config(prettyPrint: true), + engine: Engine::default( + sortByTimestamp: true, + applyElevationSmoothing: true, + elevationSmoothingThreshold: 2, + ignoreZeroElevation: false, + ), +); + +$file = $gpx->load('track.gpx'); +``` + +For fine-grained control, build the engine manually: ```php -/** - * Create Stats object for each track, segment and route - */ -public static $CALCULATE_STATS = true; - -/** - * Additional sort based on timestamp in Routes & Tracks on XML read. - * Disabled by default, data should be already sorted. - */ -public static $SORT_BY_TIMESTAMP = false; - -/** - * Default DateTime output format in JSON serialization. - */ -public static $DATETIME_FORMAT = 'c'; - -/** - * Default timezone for display. - * Data are always stored in UTC timezone. - */ -public static $DATETIME_TIMEZONE_OUTPUT = 'UTC'; - -/** - * Pretty print. - */ -public static $PRETTY_PRINT = true; - -/** - * In stats elevation calculation: ignore points with an elevation of 0 - * This can happen with some GPS software adding a point with 0 elevation - */ -public static $IGNORE_ELEVATION_0 = true; - -/** - * Apply elevation gain/loss smoothing? If true, the threshold in - * ELEVATION_SMOOTHING_THRESHOLD and ELEVATION_SMOOTHING_SPIKES_THRESHOLD (if not null) applies - */ -public static $APPLY_ELEVATION_SMOOTHING = false; - -/** - * if APPLY_ELEVATION_SMOOTHING is true - * the minimum elevation difference between considered points in meters - */ -public static $ELEVATION_SMOOTHING_THRESHOLD = 2; - -/** - * if APPLY_ELEVATION_SMOOTHING is true - * the maximum elevation difference between considered points in meters - */ -public static $ELEVATION_SMOOTHING_SPIKES_THRESHOLD = null; - -/** - * Apply distance calculation smoothing? If true, the threshold in - * DISTANCE_SMOOTHING_THRESHOLD applies - */ -public static $APPLY_DISTANCE_SMOOTHING = false; - -/** - * if APPLY_DISTANCE_SMOOTHING is true - * the minimum distance between considered points in meters - */ -public static $DISTANCE_SMOOTHING_THRESHOLD = 2; +use phpGPX\Analysis\Engine; +use phpGPX\Analysis\DistanceAnalyzer; +use phpGPX\Analysis\ElevationAnalyzer; +use phpGPX\Analysis\AltitudeAnalyzer; +use phpGPX\Analysis\TimestampAnalyzer; +use phpGPX\Analysis\BoundsAnalyzer; + +$engine = (new Engine()) + ->addAnalyzer(new DistanceAnalyzer(applySmoothing: true, smoothingThreshold: 3)) + ->addAnalyzer(new ElevationAnalyzer(applySmoothing: true, spikesThreshold: 100)) + ->addAnalyzer(new AltitudeAnalyzer()) + ->addAnalyzer(new TimestampAnalyzer()) + ->addAnalyzer(new BoundsAnalyzer()); + +$gpx->setEngine($engine); ``` -This library started as part of my job at [BACKBONE, s.r.o.](https://www.backbone.sk/en/). +This library started as part of my job at [BACKBONE, s.r.o.](https://www.backbone.sk/en/). Thank you very much for their support! ## License -This project is licensed under the terms of the MIT license. +This project is licensed under the terms of the MIT license. \ No newline at end of file diff --git a/docs/00_Getting_Started/02_Quick_Start.md b/docs/00_Getting_Started/02_Quick_Start.md index c439045..5887d83 100644 --- a/docs/00_Getting_Started/02_Quick_Start.md +++ b/docs/00_Getting_Started/02_Quick_Start.md @@ -4,15 +4,18 @@ ```php use phpGPX\phpGPX; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX(engine: Engine::default()); -$gpx = new phpGPX(); $file = $gpx->load('path/to/file.gpx'); ``` You can also parse GPX data from a string: ```php -$gpx = new phpGPX(); +$gpx = new phpGPX(engine: engine::default()); + $xml = file_get_contents('path/to/file.gpx'); $file = $gpx->parse($xml); ``` @@ -28,9 +31,10 @@ foreach ($file->waypoints as $waypoint) { } // Tracks - ordered lists of points recorded by a GPS device +// $track->stats is populated because engine was provided above foreach ($file->tracks as $track) { echo $track->name . "\n"; - echo "Distance: " . $track->stats->distance . " m\n"; + echo "Distance: " . round($track->stats->distance) . " m\n"; foreach ($track->segments as $segment) { foreach ($segment->points as $point) { diff --git a/docs/01_Usage/01_Loading_Files.md b/docs/01_Usage/01_Loading_Files.md index 4b16473..7719b4a 100644 --- a/docs/01_Usage/01_Loading_Files.md +++ b/docs/01_Usage/01_Loading_Files.md @@ -26,20 +26,36 @@ $gpx = new phpGPX(); $file = $gpx->parse($xml); ``` -## With custom configuration +## With statistics -Pass a `Config` object to customize parsing behavior: +Statistics are not calculated by default. Pass a `engine` to populate `$track->stats`, `$segment->stats`, and `$route->stats`: ```php use phpGPX\phpGPX; -use phpGPX\Config; +use phpGPX\Analysis\Engine; -$gpx = new phpGPX(new Config( - calculateStats: false, - sortByTimestamp: true, -)); +$gpx = new phpGPX(engine: Engine::default()); -$file = $gpx->load('/path/to/track.gpx'); +$file = $gpx->load('track.gpx'); + +foreach ($file->tracks as $track) { + echo "Distance: " . round($track->stats->distance) . " m\n"; +} +``` + +Without the engine, `$track->stats` will be `null`. + +## Sorting points by timestamp + +If your GPX file has out-of-order points, enable sorting on the engine: + +```php +use phpGPX\phpGPX; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX(engine: Engine::default(sortByTimestamp: true)); + +$file = $gpx->load('track.gpx'); ``` ## What gets parsed @@ -52,25 +68,13 @@ When loading a GPX file, phpGPX processes: - **Routes** (``) - containing route points (``) - **Extensions** - Garmin TrackPointExtension (heart rate, temperature, cadence) and unsupported extensions preserved as key-value pairs -## Automatic statistics - -By default, statistics are calculated automatically when loading a file. This includes distance, elevation gain/loss, duration, speed, and pace for each track, segment, and route. - -To disable automatic stats calculation: - -```php -use phpGPX\Config; +## Processing pipeline -$gpx = new phpGPX(new Config(calculateStats: false)); -$file = $gpx->load('track.gpx'); -// $file->tracks[0]->stats will be null -``` - -You can recalculate stats manually at any time: - -```php -use phpGPX\Config; +After parsing, the `engine` (if provided) runs a single-pass analysis over all points: -$config = new Config(); -$file->tracks[0]->recalculateStats($config); +```mermaid +flowchart LR + A[XML / string input] --> B[Parse to GpxFile] + B --> C["engine (single pass)"] + C --> D[Return GpxFile] ``` \ No newline at end of file diff --git a/docs/01_Usage/02_Creating_Files.md b/docs/01_Usage/02_Creating_Files.md index d31416e..8bdfc5d 100644 --- a/docs/01_Usage/02_Creating_Files.md +++ b/docs/01_Usage/02_Creating_Files.md @@ -5,7 +5,6 @@ You can build GPX files programmatically. ## Building a track from scratch ```php -use phpGPX\Config; use phpGPX\Models\GpxFile; use phpGPX\Models\Metadata; use phpGPX\Models\Point; @@ -44,20 +43,28 @@ foreach ($points as $data) { } $track->segments[] = $segment; - -// Calculate statistics -$track->recalculateStats(new Config()); - $gpxFile->tracks[] = $track; -// Save +// Save to file $gpxFile->save('morning_run.gpx', phpGPX::XML_FORMAT); ``` +## Calculating stats after building + +Models are pure data containers — they do not calculate stats. To populate `$track->stats` and `$segment->stats` on a file you have built programmatically, call `engine` directly: + +```php +use phpGPX\Analysis\Engine; + +// Build the file as above, then: +$gpxFile = Engine::default()->process($gpxFile); + +echo "Distance: " . round($gpxFile->tracks[0]->stats->distance) . " m\n"; +``` + ## Building a route ```php -use phpGPX\Config; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; use phpGPX\Models\Route; @@ -83,8 +90,6 @@ foreach ($waypoints as $data) { $route->points[] = $point; } -$route->recalculateStats(new Config()); - $gpxFile->routes[] = $route; $gpxFile->save('trail.gpx', phpGPX::XML_FORMAT); ``` diff --git a/docs/01_Usage/03_Statistics.md b/docs/01_Usage/03_Statistics.md index 0664045..4ceae83 100644 --- a/docs/01_Usage/03_Statistics.md +++ b/docs/01_Usage/03_Statistics.md @@ -1,33 +1,54 @@ # Statistics -phpGPX automatically calculates statistics for tracks, segments, and routes when loading GPX files. +Statistics are calculated by `engine`. Models (`Track`, `Segment`, `Route`) are pure data containers — they do not calculate stats themselves. + +## Enabling statistics + +```php +use phpGPX\phpGPX; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX(engine: Engine::default()); + +$file = $gpx->load('track.gpx'); +``` + +Without `engine`, `$track->stats`, `$segment->stats`, and `$route->stats` will all be `null`. ## Available statistics The `Stats` object provides: -| Property | Type | Description | -|----------|------|-------------| -| `distance` | float | Distance in meters (2D, horizontal only) | -| `realDistance` | float | Distance in meters including elevation changes (3D) | -| `averageSpeed` | float | Average speed in m/s | -| `averagePace` | float | Average pace in s/km | -| `minAltitude` | float | Minimum elevation in meters | -| `maxAltitude` | float | Maximum elevation in meters | -| `cumulativeElevationGain` | float | Total ascent in meters | -| `cumulativeElevationLoss` | float | Total descent in meters | -| `startedAt` | DateTime | Timestamp of first point | -| `finishedAt` | DateTime | Timestamp of last point | -| `duration` | float | Total duration in seconds | - -Coordinate properties are also available: `startedAtCoords`, `finishedAtCoords`, `minAltitudeCoords`, `maxAltitudeCoords` — each an array with `lat` and `lng` keys. +| Property | Type | Analyzer | +|----------|------|----------| +| `distance` | float | `DistanceAnalyzer` — 2D ground distance in meters | +| `realDistance` | float | `DistanceAnalyzer` — 3D distance including elevation | +| `averageSpeed` | float | Derived — distance / duration (m/s) | +| `averagePace` | float | Derived — duration / distance (s/km) | +| `minAltitude` | float | `AltitudeAnalyzer` — minimum elevation in meters | +| `maxAltitude` | float | `AltitudeAnalyzer` — maximum elevation in meters | +| `cumulativeElevationGain` | float | `ElevationAnalyzer` — total ascent in meters | +| `cumulativeElevationLoss` | float | `ElevationAnalyzer` — total descent in meters | +| `startedAt` | DateTime | `TimestampAnalyzer` — first non-null timestamp | +| `finishedAt` | DateTime | `TimestampAnalyzer` — last non-null timestamp | +| `duration` | float | Derived — finishedAt - startedAt (seconds) | +| `bounds` | Bounds | `BoundsAnalyzer` — lat/lon bounding box | +| `movingDuration` | float | `MovementAnalyzer` — time in motion (seconds) | +| `movingAverageSpeed` | float | Derived — distance / movingDuration (m/s) | +| `averageHeartRate` | float | `TrackPointExtensionAnalyzer` — avg HR (bpm) | +| `maxHeartRate` | float | `TrackPointExtensionAnalyzer` — peak HR (bpm) | +| `averageCadence` | float | `TrackPointExtensionAnalyzer` — avg cadence (rpm) | +| `averageTemperature` | float | `TrackPointExtensionAnalyzer` — avg temp (C) | + +Coordinate properties: `startedAtCoords`, `finishedAtCoords`, `minAltitudeCoords`, `maxAltitudeCoords` — each an array with `lat` and `lng` keys. ## Accessing statistics ```php use phpGPX\phpGPX; +use phpGPX\Analysis\Engine; -$gpx = new phpGPX(); +$gpx = new phpGPX(engine: Engine::default()); $file = $gpx->load('track.gpx'); foreach ($file->tracks as $track) { @@ -46,54 +67,125 @@ foreach ($file->tracks as $track) { } ``` -## Recalculating statistics +## The engine + +The engine walks the GPX structure **once** and dispatches each point to all registered analyzers in a single pass. No redundant iteration. -After modifying points, recalculate by passing a `Config` object: +### Quick start with defaults ```php -use phpGPX\Config; +$gpx = new phpGPX(engine: engine::default()); +``` + +### Customizing via the factory -$config = new Config(); -$track->recalculateStats($config); +```php +$gpx = new phpGPX(engine: engine::default( + sortByTimestamp: true, + applyElevationSmoothing: true, + elevationSmoothingThreshold: 2, + ignoreZeroElevation: true, + speedThreshold: 1.0, // m/s for movement detection +)); +``` + +### Building manually + +For fine-grained control, register only the analyzers you need: + +```php +use phpGPX\Analysis\Engine; +use phpGPX\Analysis\DistanceAnalyzer; +use phpGPX\Analysis\ElevationAnalyzer; +use phpGPX\Analysis\TimestampAnalyzer; + +$engine = (new Engine(sortByTimestamp: true)) + ->addAnalyzer(new DistanceAnalyzer()) + ->addAnalyzer(new ElevationAnalyzer(applySmoothing: true)) + ->addAnalyzer(new TimestampAnalyzer()); + +$gpx = new phpGPX(engine: $engine); ``` -For tracks, this recalculates each segment's stats first, then aggregates them. +## Built-in analyzers -## Distance smoothing +### DistanceAnalyzer -GPS noise can inflate distance measurements. Enable smoothing to filter out small movements: +Computes raw (2D) and real (3D) distance via the Haversine formula. ```php -use phpGPX\Config; +new DistanceAnalyzer( + applySmoothing: true, // filter GPS jitter + smoothingThreshold: 2, // meters — ignore movements below this +) +``` -$gpx = new phpGPX(new Config( - applyDistanceSmoothing: true, - distanceSmoothingThreshold: 2, // meters — ignore movements smaller than this -)); +### ElevationAnalyzer + +Computes cumulative elevation gain and loss. + +```php +new ElevationAnalyzer( + ignoreZeroElevation: true, // treat 0 as missing data + applySmoothing: true, // filter noise + smoothingThreshold: 2, // meters — minimum change to count + spikesThreshold: 50, // meters — maximum change to count +) ``` -## Elevation smoothing +### AltitudeAnalyzer -GPS altitude data is often noisy. Smoothing helps get more accurate elevation gain/loss: +Finds minimum and maximum altitude with coordinates. ```php -use phpGPX\Config; +new AltitudeAnalyzer(ignoreZeroElevation: true) +``` -$gpx = new phpGPX(new Config( - applyElevationSmoothing: true, - elevationSmoothingThreshold: 2, // meters — minimum change to count +### TimestampAnalyzer - // Optional: filter spikes (e.g. GPS glitches showing 100m jumps) - elevationSmoothingSpikesThreshold: 50, // meters — maximum change to count -)); +Records first and last non-null timestamps with coordinates. Duration, speed, and pace are derived by the engine. + +### BoundsAnalyzer + +Computes lat/lon bounding box at segment, track, and file level. File-level bounds include waypoints. + +### MovementAnalyzer + +Detects movement vs stopped intervals based on instantaneous speed. + +```php +new MovementAnalyzer(speedThreshold: 1.0) // 3.6 km/h — walking pace ``` -## Ignoring zero elevation +### TrackPointExtensionAnalyzer + +Aggregates Garmin TrackPointExtension sensor data (heart rate, cadence, temperature). -Some GPS devices record elevation as 0 when they lose satellite fix. Ignore these points: +## Standalone usage + +You can also use `engine` directly on a `GpxFile` you built programmatically: + +```php +$gpxFile = engine::default()->process($gpxFile); +``` + +## Full example ```php -use phpGPX\Config; +use phpGPX\phpGPX; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX(engine: Engine::default( + sortByTimestamp: true, + applyElevationSmoothing: true, + elevationSmoothingThreshold: 2, +)); + +$file = $gpx->load('track.gpx'); +``` -$gpx = new phpGPX(new Config(ignoreZeroElevation: true)); +```mermaid +flowchart LR + A[load / parse] --> B["engine (sort + single pass)"] + B --> C[GpxFile with stats] ``` \ No newline at end of file diff --git a/docs/01_Usage/04_Configuration.md b/docs/01_Usage/04_Configuration.md index 97db14f..5d071c5 100644 --- a/docs/01_Usage/04_Configuration.md +++ b/docs/01_Usage/04_Configuration.md @@ -1,34 +1,21 @@ # Configuration -phpGPX is configured through the `Config` value object, passed to the `phpGPX` constructor. Each instance carries its own configuration — there is no global state. +phpGPX is configured through two mechanisms: -## All options +1. **`Config` value object** — output formatting, passed to the `phpGPX` constructor +2. **`engine` and analyzer constructors** — processing behavior (smoothing, thresholds, sorting, etc.) + +Each `phpGPX` instance carries its own configuration — there is no global state. + +## Config options ```php use phpGPX\phpGPX; use phpGPX\Config; -$gpx = new phpGPX(new Config( - // Calculate statistics automatically on load (default: true) - calculateStats: true, - - // Sort points by timestamp when loading (default: false) - sortByTimestamp: false, - +$gpx = new phpGPX(config: new Config( // Pretty print XML and JSON output (default: true) prettyPrint: true, - - // Ignore elevation values of 0 in stats (default: false) - ignoreZeroElevation: false, - - // Distance smoothing (default: false) - applyDistanceSmoothing: false, - distanceSmoothingThreshold: 2, // meters - - // Elevation smoothing (default: false) - applyElevationSmoothing: false, - elevationSmoothingThreshold: 2, // meters - elevationSmoothingSpikesThreshold: null, // meters, or null to disable )); ``` @@ -40,19 +27,93 @@ All options have sensible defaults. Creating a `phpGPX` instance without argumen $gpx = new phpGPX(); // uses all defaults ``` +## Config properties reference + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `prettyPrint` | bool | `true` | Indent XML and JSON output | + +!!! note "Config is for output only" + Processing behavior (stats calculation, smoothing, sorting) is controlled by `engine` and analyzer constructor arguments, not by Config. + +## engine configuration + +### Using the factory (recommended) + +```php +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX(engine: Engine::default( + sortByTimestamp: true, // Sort points by time before analysis + ignoreZeroElevation: false, // Treat 0m elevation as missing + applyElevationSmoothing: true, // Enable elevation smoothing + elevationSmoothingThreshold: 2, // Minimum elevation change (m) + elevationSmoothingSpikesThreshold: 50, // Maximum change before spike rejection + applyDistanceSmoothing: true, // Enable distance smoothing + distanceSmoothingThreshold: 2, // Minimum movement (m) to count + speedThreshold: 0.5, // Movement detection threshold (m/s) +)); +``` + +### Building manually + +```php +use phpGPX\Analysis\Engine; +use phpGPX\Analysis\DistanceAnalyzer; +use phpGPX\Analysis\ElevationAnalyzer; +use phpGPX\Analysis\AltitudeAnalyzer; +use phpGPX\Analysis\TimestampAnalyzer; +use phpGPX\Analysis\BoundsAnalyzer; +use phpGPX\Analysis\MovementAnalyzer; +use phpGPX\Analysis\TrackPointExtensionAnalyzer; + +$engine = (new Engine(sortByTimestamp: true)) + ->addAnalyzer(new DistanceAnalyzer(applySmoothing: true, smoothingThreshold: 3)) + ->addAnalyzer(new ElevationAnalyzer( + applySmoothing: true, + smoothingThreshold: 5, + spikesThreshold: 100, + )) + ->addAnalyzer(new AltitudeAnalyzer(ignoreZeroElevation: true)) + ->addAnalyzer(new TimestampAnalyzer()) + ->addAnalyzer(new BoundsAnalyzer()) + ->addAnalyzer(new MovementAnalyzer(speedThreshold: 1.0)) + ->addAnalyzer(new TrackPointExtensionAnalyzer()); + +$gpx = new phpGPX(engine: $engine); +``` + +## Full example + +```php +use phpGPX\phpGPX; +use phpGPX\Config; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX( + config: new Config(prettyPrint: true), + engine: Engine::default( + sortByTimestamp: true, + applyElevationSmoothing: true, + elevationSmoothingThreshold: 5, + speedThreshold: 0.5, + ), +); + +$file = $gpx->load('track.gpx'); +``` + ## Multiple configurations Since configuration is per-instance, you can use different settings for different files: ```php -$smooth = new phpGPX(new Config( +$smooth = new phpGPX(engine: engine::default( applyElevationSmoothing: true, elevationSmoothingThreshold: 5, )); -$raw = new phpGPX(new Config( - applyElevationSmoothing: false, -)); +$raw = new phpGPX(engine: engine::default()); $smoothFile = $smooth->load('track.gpx'); $rawFile = $raw->load('track.gpx'); @@ -61,5 +122,5 @@ $rawFile = $raw->load('track.gpx'); ## Notes - Configuration is immutable after construction — `Config` properties are set once via constructor. -- The `sortByTimestamp` option is useful for GPX files where points are out of order, but is disabled by default since most files are already sorted. -- JSON output always uses ISO 8601 UTC for datetime values (GeoJSON convention). Datetime formatting is a consumer concern. \ No newline at end of file +- JSON output always uses ISO 8601 UTC for datetime values (GeoJSON convention). +- Stats are produced exclusively by `engine` and its analyzers — models are pure data containers. \ No newline at end of file diff --git a/src/phpGPX/Analysis/AbstractPointAnalyzer.php b/src/phpGPX/Analysis/AbstractPointAnalyzer.php new file mode 100644 index 0000000..eb53c19 --- /dev/null +++ b/src/phpGPX/Analysis/AbstractPointAnalyzer.php @@ -0,0 +1,58 @@ +maxSpeed = 0; + * } + * + * public function visit(Point $current, ?Point $previous): void + * { + * if ($previous === null) return; + * // ... compute speed, track max ... + * } + * + * public function end(Stats $stats): void + * { + * // Write to a custom Stats field or extension + * } + * } + * ``` + * + * @see PointAnalyzerInterface for the full lifecycle contract + */ +abstract class AbstractPointAnalyzer implements PointAnalyzerInterface +{ + public function aggregateTrack(Track $track): void + { + // No-op by default — override if your analyzer needs track-level aggregation. + } + + public function finalizeFile(GpxFile $gpxFile): void + { + // No-op by default — override for file-level post-processing. + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/AltitudeAnalyzer.php b/src/phpGPX/Analysis/AltitudeAnalyzer.php new file mode 100644 index 0000000..61b74da --- /dev/null +++ b/src/phpGPX/Analysis/AltitudeAnalyzer.php @@ -0,0 +1,102 @@ +minAltitude = null; + $this->minCoords = null; + $this->maxAltitude = null; + $this->maxCoords = null; + } + + public function visit(Point $current, ?Point $previous): void + { + $elevation = $current->elevation; + + if ($elevation === null) { + return; + } + + if ($this->ignoreZeroElevation && $elevation == 0) { + return; + } + + $coords = ['lat' => $current->latitude, 'lng' => $current->longitude]; + + if ($this->maxAltitude === null || $elevation > $this->maxAltitude) { + $this->maxAltitude = $elevation; + $this->maxCoords = $coords; + } + + if ($this->minAltitude === null || $elevation < $this->minAltitude) { + $this->minAltitude = $elevation; + $this->minCoords = $coords; + } + } + + public function end(Stats $stats): void + { + $stats->maxAltitude = $this->maxAltitude; + $stats->maxAltitudeCoords = $this->maxCoords; + $stats->minAltitude = $this->minAltitude; + $stats->minAltitudeCoords = $this->minCoords; + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + return; + } + + foreach ($track->segments as $segment) { + if ($segment->stats === null) { + continue; + } + + if ($segment->stats->maxAltitude !== null + && ($track->stats->maxAltitude === null || $segment->stats->maxAltitude > $track->stats->maxAltitude)) { + $track->stats->maxAltitude = $segment->stats->maxAltitude; + $track->stats->maxAltitudeCoords = $segment->stats->maxAltitudeCoords; + } + + if ($segment->stats->minAltitude !== null + && ($track->stats->minAltitude === null || $segment->stats->minAltitude < $track->stats->minAltitude)) { + $track->stats->minAltitude = $segment->stats->minAltitude; + $track->stats->minAltitudeCoords = $segment->stats->minAltitudeCoords; + } + } + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/BoundsAnalyzer.php b/src/phpGPX/Analysis/BoundsAnalyzer.php new file mode 100644 index 0000000..2b4c78a --- /dev/null +++ b/src/phpGPX/Analysis/BoundsAnalyzer.php @@ -0,0 +1,178 @@ +bounds` via {@see finalizeFile()}) + * + * Points with null coordinates are silently skipped. + * + * ## File-level bounds (#28) + * + * This is the only built-in analyzer that uses {@see finalizeFile()} — it + * needs to include waypoints in the file-level bounding box, and waypoints + * are not part of any track or route. + */ +class BoundsAnalyzer extends AbstractPointAnalyzer +{ + private float $minLat; + private float $maxLat; + private float $minLon; + private float $maxLon; + private bool $hasData; + + /** Accumulated file-level bounds across all tracks, routes, and waypoints. */ + private float $fileMinLat; + private float $fileMaxLat; + private float $fileMinLon; + private float $fileMaxLon; + private bool $fileHasData = false; + + public function begin(): void + { + $this->minLat = PHP_FLOAT_MAX; + $this->maxLat = -PHP_FLOAT_MAX; + $this->minLon = PHP_FLOAT_MAX; + $this->maxLon = -PHP_FLOAT_MAX; + $this->hasData = false; + } + + public function visit(Point $current, ?Point $previous): void + { + if ($current->latitude === null || $current->longitude === null) { + return; + } + + $this->hasData = true; + + if ($current->latitude < $this->minLat) { + $this->minLat = $current->latitude; + } + if ($current->latitude > $this->maxLat) { + $this->maxLat = $current->latitude; + } + if ($current->longitude < $this->minLon) { + $this->minLon = $current->longitude; + } + if ($current->longitude > $this->maxLon) { + $this->maxLon = $current->longitude; + } + + // Accumulate for file-level bounds + $this->expandFileBounds($current->latitude, $current->longitude); + } + + public function end(Stats $stats): void + { + $stats->bounds = $this->hasData + ? new Bounds($this->minLat, $this->minLon, $this->maxLat, $this->maxLon) + : null; + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + return; + } + + // Merge segment bounds into track bounds + $minLat = PHP_FLOAT_MAX; + $maxLat = -PHP_FLOAT_MAX; + $minLon = PHP_FLOAT_MAX; + $maxLon = -PHP_FLOAT_MAX; + $hasData = false; + + foreach ($track->segments as $segment) { + if ($segment->stats?->bounds === null) { + continue; + } + + $hasData = true; + $b = $segment->stats->bounds; + + if ($b->minLatitude < $minLat) { + $minLat = $b->minLatitude; + } + if ($b->maxLatitude > $maxLat) { + $maxLat = $b->maxLatitude; + } + if ($b->minLongitude < $minLon) { + $minLon = $b->minLongitude; + } + if ($b->maxLongitude > $maxLon) { + $maxLon = $b->maxLongitude; + } + } + + $track->stats->bounds = $hasData + ? new Bounds($minLat, $minLon, $maxLat, $maxLon) + : null; + } + + public function finalizeFile(GpxFile $gpxFile): void + { + // Include waypoints in file-level bounds + foreach ($gpxFile->waypoints as $waypoint) { + if ($waypoint->latitude !== null && $waypoint->longitude !== null) { + $this->expandFileBounds($waypoint->latitude, $waypoint->longitude); + } + } + + if (!$this->fileHasData) { + return; + } + + if ($gpxFile->metadata === null) { + $gpxFile->metadata = new Metadata(); + } + + $gpxFile->metadata->bounds = new Bounds( + $this->fileMinLat, + $this->fileMinLon, + $this->fileMaxLat, + $this->fileMaxLon, + ); + } + + /** + * Expand the file-level bounding box with a coordinate. + */ + private function expandFileBounds(float $lat, float $lon): void + { + if (!$this->fileHasData) { + $this->fileMinLat = $lat; + $this->fileMaxLat = $lat; + $this->fileMinLon = $lon; + $this->fileMaxLon = $lon; + $this->fileHasData = true; + return; + } + + if ($lat < $this->fileMinLat) { + $this->fileMinLat = $lat; + } + if ($lat > $this->fileMaxLat) { + $this->fileMaxLat = $lat; + } + if ($lon < $this->fileMinLon) { + $this->fileMinLon = $lon; + } + if ($lon > $this->fileMaxLon) { + $this->fileMaxLon = $lon; + } + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/DistanceAnalyzer.php b/src/phpGPX/Analysis/DistanceAnalyzer.php new file mode 100644 index 0000000..44174dd --- /dev/null +++ b/src/phpGPX/Analysis/DistanceAnalyzer.php @@ -0,0 +1,106 @@ +difference` — raw distance from the previous accepted point + * - `$point->distance` — cumulative raw distance from the start + * + * ## Track aggregation + * + * Track distance = sum of segment distances. + */ +class DistanceAnalyzer extends AbstractPointAnalyzer +{ + private float $rawDistance; + private float $realDistance; + private ?Point $lastAcceptedPoint; + + public function __construct( + private bool $applySmoothing = false, + private int $smoothingThreshold = 2, + ) {} + + public function begin(): void + { + $this->rawDistance = 0; + $this->realDistance = 0; + $this->lastAcceptedPoint = null; + } + + public function visit(Point $current, ?Point $previous): void + { + if ($this->lastAcceptedPoint === null) { + $this->lastAcceptedPoint = $current; + return; + } + + $rawDiff = GeoHelper::getRawDistance($this->lastAcceptedPoint, $current); + $realDiff = GeoHelper::getRealDistance($this->lastAcceptedPoint, $current); + + $current->difference = $rawDiff; + + if ($this->applySmoothing) { + if ($rawDiff > $this->smoothingThreshold) { + $this->rawDistance += $rawDiff; + $this->realDistance += $realDiff; + $this->lastAcceptedPoint = $current; + } + } else { + $this->rawDistance += $rawDiff; + $this->realDistance += $realDiff; + $this->lastAcceptedPoint = $current; + } + + $current->distance = $this->rawDistance; + } + + public function end(Stats $stats): void + { + $stats->distance = $this->rawDistance; + $stats->realDistance = $this->realDistance; + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + return; + } + + $totalRaw = 0.0; + $totalReal = 0.0; + + foreach ($track->segments as $segment) { + $totalRaw += $segment->stats->distance ?? 0; + $totalReal += $segment->stats->realDistance ?? 0; + } + + $track->stats->distance = $totalRaw; + $track->stats->realDistance = $totalReal; + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/ElevationAnalyzer.php b/src/phpGPX/Analysis/ElevationAnalyzer.php new file mode 100644 index 0000000..2fc31f3 --- /dev/null +++ b/src/phpGPX/Analysis/ElevationAnalyzer.php @@ -0,0 +1,118 @@ +gain = 0; + $this->loss = 0; + $this->lastElevation = null; + } + + public function visit(Point $current, ?Point $previous): void + { + $elevation = $current->elevation; + + if ($elevation === null) { + return; + } + + if ($this->ignoreZeroElevation && $elevation == 0) { + return; + } + + if ($this->lastElevation === null) { + $this->lastElevation = $elevation; + return; + } + + $delta = $elevation - $this->lastElevation; + + if ($this->applySmoothing) { + $absDelta = abs($delta); + + if ($absDelta > $this->smoothingThreshold + && ($this->spikesThreshold === null || $absDelta < $this->spikesThreshold)) { + $this->gain += ($delta > 0) ? $delta : 0; + $this->loss += ($delta < 0) ? abs($delta) : 0; + $this->lastElevation = $elevation; + } + } else { + $this->gain += ($delta > 0) ? $delta : 0; + $this->loss += ($delta < 0) ? abs($delta) : 0; + $this->lastElevation = $elevation; + } + } + + public function end(Stats $stats): void + { + $stats->cumulativeElevationGain = $this->gain; + $stats->cumulativeElevationLoss = $this->loss; + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + return; + } + + $totalGain = 0.0; + $totalLoss = 0.0; + + foreach ($track->segments as $segment) { + $totalGain += $segment->stats->cumulativeElevationGain ?? 0; + $totalLoss += $segment->stats->cumulativeElevationLoss ?? 0; + } + + $track->stats->cumulativeElevationGain = $totalGain; + $track->stats->cumulativeElevationLoss = $totalLoss; + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/Engine.php b/src/phpGPX/Analysis/Engine.php new file mode 100644 index 0000000..ead9851 --- /dev/null +++ b/src/phpGPX/Analysis/Engine.php @@ -0,0 +1,307 @@ +load('track.gpx'); + * $file = Engine::default()->process($file); + * ``` + * + * Custom configuration: + * + * ```php + * $engine = Engine::default( + * sortByTimestamp: true, + * applyElevationSmoothing: true, + * elevationSmoothingThreshold: 2, + * ); + * $file = $engine->process($file); + * ``` + * + * Manual analyzer selection: + * + * ```php + * $engine = (new Engine()) + * ->addAnalyzer(new DistanceAnalyzer()) + * ->addAnalyzer(new ElevationAnalyzer(applySmoothing: true)) + * ->addAnalyzer(new BoundsAnalyzer()); + * ``` + * + * ## Derived stats + * + * After all analyzers write their results, the engine computes derived values + * that depend on multiple analyzers' output (e.g. average speed = distance / duration). + * This removes inter-analyzer ordering dependencies. + * + * @see PointAnalyzerInterface for the analyzer lifecycle + * @see AbstractPointAnalyzer for a convenient base class + */ +class Engine +{ + /** @var PointAnalyzerInterface[] */ + private array $analyzers = []; + + /** + * @param bool $sortByTimestamp Sort points by timestamp before analysis. + * Useful for GPX files with out-of-order points. + */ + public function __construct( + private bool $sortByTimestamp = false, + ) {} + + /** + * Register an analyzer to participate in the single-pass computation. + * + * Analyzers are called in registration order. For most analyzers, order + * does not matter — each writes to its own Stats fields. The engine + * computes derived stats (speed, pace) after all analyzers finish. + * + * @return $this Fluent interface + */ + public function addAnalyzer(PointAnalyzerInterface $analyzer): self + { + $this->analyzers[] = $analyzer; + return $this; + } + + /** + * Create an engine with the standard set of analyzers. + * + * This is the recommended way to get started — it registers all built-in + * analyzers with sensible defaults. Pass named arguments to customize + * specific analyzers' behavior. + * + * ```php + * // All defaults + * $engine = Engine::default(); + * + * // Custom elevation smoothing + sorting + * $engine = Engine::default( + * sortByTimestamp: true, + * applyElevationSmoothing: true, + * elevationSmoothingThreshold: 3, + * ); + * ``` + */ + public static function default( + bool $sortByTimestamp = false, + bool $applyDistanceSmoothing = false, + int $distanceSmoothingThreshold = 2, + bool $ignoreZeroElevation = false, + bool $applyElevationSmoothing = false, + int $elevationSmoothingThreshold = 2, + ?int $elevationSmoothingSpikesThreshold = null, + float $speedThreshold = 0.5, + ): self { + return (new self(sortByTimestamp: $sortByTimestamp)) + ->addAnalyzer(new DistanceAnalyzer( + applySmoothing: $applyDistanceSmoothing, + smoothingThreshold: $distanceSmoothingThreshold, + )) + ->addAnalyzer(new ElevationAnalyzer( + ignoreZeroElevation: $ignoreZeroElevation, + applySmoothing: $applyElevationSmoothing, + smoothingThreshold: $elevationSmoothingThreshold, + spikesThreshold: $elevationSmoothingSpikesThreshold, + )) + ->addAnalyzer(new AltitudeAnalyzer( + ignoreZeroElevation: $ignoreZeroElevation, + )) + ->addAnalyzer(new TimestampAnalyzer()) + ->addAnalyzer(new BoundsAnalyzer()) + ->addAnalyzer(new MovementAnalyzer( + speedThreshold: $speedThreshold, + )) + ->addAnalyzer(new TrackPointExtensionAnalyzer()); + } + + /** + * Process the GPX file: optionally sort, then run single-pass analysis. + */ + public function process(GpxFile $gpxFile): GpxFile + { + if ($this->sortByTimestamp) { + $this->sortPoints($gpxFile); + } + + $this->processTracks($gpxFile); + $this->processRoutes($gpxFile); + $this->finalizeFile($gpxFile); + + return $gpxFile; + } + + /** + * Process all tracks: segments → points in a single pass per segment, + * then aggregate segment stats into track stats. + */ + private function processTracks(GpxFile $gpxFile): void + { + foreach ($gpxFile->tracks as $track) { + $track->stats = new Stats(); + + if (empty($track->segments)) { + continue; + } + + foreach ($track->segments as $segment) { + $segment->stats = new Stats(); + + if (!empty($segment->points)) { + $this->analyzePoints($segment->points, $segment->stats); + } + } + + foreach ($this->analyzers as $analyzer) { + $analyzer->aggregateTrack($track); + } + + $this->computeDerivedStats($track->stats); + } + } + + /** + * Process all routes: points in a single pass per route. + */ + private function processRoutes(GpxFile $gpxFile): void + { + foreach ($gpxFile->routes as $route) { + $route->stats = new Stats(); + + if (!empty($route->points)) { + $this->analyzePoints($route->points, $route->stats); + } + } + } + + /** + * Run all analyzers over a set of points in a single pass. + * + * This is the core of the engine — one loop over points, all analyzers + * participate simultaneously. + * + * @param Point[] $points The points to analyze + * @param Stats $stats The Stats object to populate + */ + private function analyzePoints(array $points, Stats $stats): void + { + // Phase 1: Reset all analyzers + foreach ($this->analyzers as $analyzer) { + $analyzer->begin(); + } + + // Phase 2: Single pass — every analyzer sees every point + $previous = null; + foreach ($points as $point) { + foreach ($this->analyzers as $analyzer) { + $analyzer->visit($point, $previous); + } + $previous = $point; + } + + // Phase 3: Write results to stats + foreach ($this->analyzers as $analyzer) { + $analyzer->end($stats); + } + + // Phase 4: Compute derived stats (speed, pace) from combined results + $this->computeDerivedStats($stats); + } + + /** + * Sort points by timestamp within each segment and route. + * + * Skips segments/routes where the first point has no timestamp + * (assumes all points are either timestamped or not). + */ + private function sortPoints(GpxFile $gpxFile): void + { + $compare = fn($a, $b) => $a->time <=> $b->time; + + foreach ($gpxFile->tracks as $track) { + foreach ($track->segments as $segment) { + if (!empty($segment->points) && $segment->points[0]->time !== null) { + usort($segment->points, $compare); + } + } + } + + foreach ($gpxFile->routes as $route) { + if (!empty($route->points) && $route->points[0]->time !== null) { + usort($route->points, $compare); + } + } + } + + /** + * File-level finalization — called after all tracks and routes are processed. + */ + private function finalizeFile(GpxFile $gpxFile): void + { + foreach ($this->analyzers as $analyzer) { + $analyzer->finalizeFile($gpxFile); + } + } + + /** + * Compute values that depend on multiple analyzers' output. + * + * Average speed and pace require both distance (from DistanceAnalyzer) + * and duration (from TimestampAnalyzer). Rather than creating ordering + * dependencies between analyzers, the engine computes these derived + * values after all analyzers have written their results. + */ + private function computeDerivedStats(Stats $stats): void + { + if ($stats->startedAt instanceof \DateTime && $stats->finishedAt instanceof \DateTime) { + $stats->duration = abs( + $stats->finishedAt->getTimestamp() - $stats->startedAt->getTimestamp() + ); + + if ($stats->duration != 0 && $stats->distance !== null) { + $stats->averageSpeed = $stats->distance / $stats->duration; + } + + if ($stats->distance !== null && $stats->distance != 0) { + $stats->averagePace = $stats->duration / ($stats->distance / 1000); + } + } + + if ($stats->movingDuration !== null && $stats->movingDuration > 0 && $stats->distance !== null) { + $stats->movingAverageSpeed = $stats->distance / $stats->movingDuration; + } + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/MovementAnalyzer.php b/src/phpGPX/Analysis/MovementAnalyzer.php new file mode 100644 index 0000000..35371d9 --- /dev/null +++ b/src/phpGPX/Analysis/MovementAnalyzer.php @@ -0,0 +1,99 @@ +movingSeconds = 0; + $this->hasTimestamps = false; + } + + public function visit(Point $current, ?Point $previous): void + { + if ($previous === null) { + return; + } + + if ($previous->time === null || $current->time === null) { + return; + } + + $this->hasTimestamps = true; + + $timeDelta = abs($current->time->getTimestamp() - $previous->time->getTimestamp()); + + if ($timeDelta === 0) { + return; + } + + $distance = GeoHelper::getRawDistance($previous, $current); + $speed = $distance / $timeDelta; + + if ($speed >= $this->speedThreshold) { + $this->movingSeconds += $timeDelta; + } + } + + public function end(Stats $stats): void + { + $stats->movingDuration = $this->hasTimestamps ? $this->movingSeconds : null; + // movingAverageSpeed is computed by the engine (depends on distance) + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + return; + } + + $totalMoving = 0.0; + $hasData = false; + + foreach ($track->segments as $segment) { + if ($segment->stats?->movingDuration !== null) { + $totalMoving += $segment->stats->movingDuration; + $hasData = true; + } + } + + $track->stats->movingDuration = $hasData ? $totalMoving : null; + // movingAverageSpeed is computed by the engine + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/PointAnalyzerInterface.php b/src/phpGPX/Analysis/PointAnalyzerInterface.php new file mode 100644 index 0000000..cb67a96 --- /dev/null +++ b/src/phpGPX/Analysis/PointAnalyzerInterface.php @@ -0,0 +1,90 @@ +segments[*]->stats`. + * + * @param Track $track The track with fully populated segment stats + */ + public function aggregateTrack(Track $track): void; + + /** + * Optional file-level post-processing. + * + * Called once after all tracks and routes have been processed. Use this + * for cross-cutting concerns like computing file-level bounds that include + * waypoints, or setting metadata properties. + * + * Most analyzers can leave this as a no-op. + * + * @param GpxFile $gpxFile The fully processed GPX file + */ + public function finalizeFile(GpxFile $gpxFile): void; +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/TimestampAnalyzer.php b/src/phpGPX/Analysis/TimestampAnalyzer.php new file mode 100644 index 0000000..f9d8fee --- /dev/null +++ b/src/phpGPX/Analysis/TimestampAnalyzer.php @@ -0,0 +1,91 @@ +startedAt = null; + $this->startedAtCoords = null; + $this->finishedAt = null; + $this->finishedAtCoords = null; + } + + public function visit(Point $current, ?Point $previous): void + { + if (!$current->time instanceof \DateTime) { + return; + } + + $coords = ['lat' => $current->latitude, 'lng' => $current->longitude]; + + // First non-null timestamp becomes startedAt + if ($this->startedAt === null) { + $this->startedAt = $current->time; + $this->startedAtCoords = $coords; + } + + // Every non-null timestamp updates finishedAt (last one wins) + $this->finishedAt = $current->time; + $this->finishedAtCoords = $coords; + } + + public function end(Stats $stats): void + { + $stats->startedAt = $this->startedAt; + $stats->startedAtCoords = $this->startedAtCoords; + $stats->finishedAt = $this->finishedAt; + $stats->finishedAtCoords = $this->finishedAtCoords; + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + return; + } + + foreach ($track->segments as $segment) { + if ($segment->stats === null) { + continue; + } + + if ($segment->stats->startedAt instanceof \DateTime + && ($track->stats->startedAt === null || $segment->stats->startedAt < $track->stats->startedAt)) { + $track->stats->startedAt = $segment->stats->startedAt; + $track->stats->startedAtCoords = $segment->stats->startedAtCoords; + } + + if ($segment->stats->finishedAt instanceof \DateTime + && ($track->stats->finishedAt === null || $segment->stats->finishedAt > $track->stats->finishedAt)) { + $track->stats->finishedAt = $segment->stats->finishedAt; + $track->stats->finishedAtCoords = $segment->stats->finishedAtCoords; + } + } + } +} \ No newline at end of file diff --git a/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php new file mode 100644 index 0000000..74cb437 --- /dev/null +++ b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php @@ -0,0 +1,153 @@ +hrSum = 0; + $this->hrCount = 0; + $this->hrMax = null; + $this->cadSum = 0; + $this->cadCount = 0; + $this->tempSum = 0; + $this->tempCount = 0; + } + + public function visit(Point $current, ?Point $previous): void + { + $ext = $current->extensions?->trackPointExtension; + + if ($ext === null) { + return; + } + + $hr = $ext->hr ?? null; + $cad = $ext->cad ?? null; + $aTemp = $ext->aTemp ?? null; + + if ($hr !== null) { + $this->hrSum += $hr; + $this->hrCount++; + $this->trackHrSum += $hr; + $this->trackHrCount++; + + if ($this->hrMax === null || $hr > $this->hrMax) { + $this->hrMax = $hr; + } + if ($this->trackHrMax === null || $hr > $this->trackHrMax) { + $this->trackHrMax = $hr; + } + } + + if ($cad !== null) { + $this->cadSum += $cad; + $this->cadCount++; + $this->trackCadSum += $cad; + $this->trackCadCount++; + } + + if ($aTemp !== null) { + $this->tempSum += $aTemp; + $this->tempCount++; + $this->trackTempSum += $aTemp; + $this->trackTempCount++; + } + } + + public function end(Stats $stats): void + { + if ($this->hrCount > 0) { + $stats->averageHeartRate = $this->hrSum / $this->hrCount; + $stats->maxHeartRate = $this->hrMax; + } + + if ($this->cadCount > 0) { + $stats->averageCadence = $this->cadSum / $this->cadCount; + } + + if ($this->tempCount > 0) { + $stats->averageTemperature = $this->tempSum / $this->tempCount; + } + } + + public function aggregateTrack(Track $track): void + { + if ($track->stats === null) { + $this->resetTrackAccumulators(); + return; + } + + if ($this->trackHrCount > 0) { + $track->stats->averageHeartRate = $this->trackHrSum / $this->trackHrCount; + $track->stats->maxHeartRate = $this->trackHrMax; + } + + if ($this->trackCadCount > 0) { + $track->stats->averageCadence = $this->trackCadSum / $this->trackCadCount; + } + + if ($this->trackTempCount > 0) { + $track->stats->averageTemperature = $this->trackTempSum / $this->trackTempCount; + } + + $this->resetTrackAccumulators(); + } + + private function resetTrackAccumulators(): void + { + $this->trackHrSum = 0; + $this->trackHrCount = 0; + $this->trackHrMax = null; + $this->trackCadSum = 0; + $this->trackCadCount = 0; + $this->trackTempSum = 0; + $this->trackTempCount = 0; + } +} \ No newline at end of file diff --git a/src/phpGPX/Config.php b/src/phpGPX/Config.php index 9ecb381..bccfce6 100644 --- a/src/phpGPX/Config.php +++ b/src/phpGPX/Config.php @@ -10,31 +10,7 @@ class Config { public function __construct( - /** Calculate stats for tracks, segments and routes */ - public bool $calculateStats = true, - - /** Sort points by timestamp in Routes & Tracks on XML read */ - public bool $sortByTimestamp = false, - /** Pretty print XML and JSON output */ public bool $prettyPrint = true, - - /** Ignore points with elevation of 0 in stats calculation */ - public bool $ignoreZeroElevation = false, - - /** Apply elevation gain/loss smoothing */ - public bool $applyElevationSmoothing = false, - - /** Minimum elevation difference in meters for smoothing */ - public int $elevationSmoothingThreshold = 2, - - /** Maximum elevation difference in meters for spike filtering */ - public ?int $elevationSmoothingSpikesThreshold = null, - - /** Apply distance calculation smoothing */ - public bool $applyDistanceSmoothing = false, - - /** Minimum distance in meters between considered points for smoothing */ - public int $distanceSmoothingThreshold = 2, ) {} } \ No newline at end of file diff --git a/src/phpGPX/Helpers/DateTimeHelper.php b/src/phpGPX/Helpers/DateTimeHelper.php index 03355fa..2110b3a 100644 --- a/src/phpGPX/Helpers/DateTimeHelper.php +++ b/src/phpGPX/Helpers/DateTimeHelper.php @@ -6,28 +6,12 @@ namespace phpGPX\Helpers; -use phpGPX\Models\Point; - /** * Class DateTimeHelper * @package phpGPX\Helpers */ class DateTimeHelper { - - /** - * @param Point $point1 - * @param Point $point2 - * @return bool|int - */ - public static function comparePointsByTimestamp(Point $point1, Point $point2): bool|int - { - if ($point1->time == $point2->time) { - return 0; - } - return $point1->time > $point2->time; - } - /** * @param $datetime * @param string $format diff --git a/src/phpGPX/Helpers/DistanceCalculator.php b/src/phpGPX/Helpers/DistanceCalculator.php index 1819317..9a8ecf3 100644 --- a/src/phpGPX/Helpers/DistanceCalculator.php +++ b/src/phpGPX/Helpers/DistanceCalculator.php @@ -10,25 +10,18 @@ namespace phpGPX\Helpers; -use phpGPX\Config; use phpGPX\Models\Point; class DistanceCalculator { - /** @var Point[] */ - private array $points; - - private Config $config; - /** * @param Point[] $points - * @param Config $config */ - public function __construct(array $points, Config $config) - { - $this->points = $points; - $this->config = $config; - } + public function __construct( + private array $points, + private bool $applySmoothing = false, + private int $smoothingThreshold = 2, + ) {} public function getRawDistance(): float { @@ -58,10 +51,10 @@ private function calculate(array $strategy): float $curPoint->difference = call_user_func($strategy, $lastConsideredPoint, $curPoint); - if ($this->config->applyDistanceSmoothing) { + if ($this->applySmoothing) { $differenceFromLastConsideredPoint = call_user_func($strategy, $curPoint, $lastConsideredPoint); - if ($differenceFromLastConsideredPoint > $this->config->distanceSmoothingThreshold) { + if ($differenceFromLastConsideredPoint > $this->smoothingThreshold) { $distance += $differenceFromLastConsideredPoint; $lastConsideredPoint = $curPoint; } diff --git a/src/phpGPX/Helpers/ElevationGainLossCalculator.php b/src/phpGPX/Helpers/ElevationGainLossCalculator.php index 9af1ef1..a6dc978 100644 --- a/src/phpGPX/Helpers/ElevationGainLossCalculator.php +++ b/src/phpGPX/Helpers/ElevationGainLossCalculator.php @@ -8,18 +8,21 @@ namespace phpGPX\Helpers; -use phpGPX\Config; use phpGPX\Models\Point; class ElevationGainLossCalculator { /** * @param Point[] $points - * @param Config $config * @return array [cumulativeElevationGain, cumulativeElevationLoss] */ - public static function calculate(array $points, Config $config): array - { + public static function calculate( + array $points, + bool $ignoreZeroElevation = false, + bool $applySmoothing = false, + int $smoothingThreshold = 2, + ?int $spikesThreshold = null, + ): array { $cumulativeElevationGain = 0; $cumulativeElevationLoss = 0; @@ -34,7 +37,7 @@ public static function calculate(array $points, Config $config): array continue; } - if ($config->ignoreZeroElevation && $curElevation == 0) { + if ($ignoreZeroElevation && $curElevation == 0) { continue; } @@ -45,16 +48,16 @@ public static function calculate(array $points, Config $config): array $elevationDelta = $curElevation - $lastConsideredElevation; - if ($config->applyElevationSmoothing && - abs($elevationDelta) > $config->elevationSmoothingThreshold && - ($config->elevationSmoothingSpikesThreshold === null || abs($elevationDelta) < $config->elevationSmoothingSpikesThreshold)) { + if ($applySmoothing && + abs($elevationDelta) > $smoothingThreshold && + ($spikesThreshold === null || abs($elevationDelta) < $spikesThreshold)) { $cumulativeElevationGain += ($elevationDelta > 0) ? $elevationDelta : 0; $cumulativeElevationLoss += ($elevationDelta < 0) ? abs($elevationDelta) : 0; $lastConsideredElevation = $curElevation; } - if (!$config->applyElevationSmoothing) { + if (!$applySmoothing) { $cumulativeElevationGain += ($elevationDelta > 0) ? $elevationDelta : 0; $cumulativeElevationLoss += ($elevationDelta < 0) ? abs($elevationDelta) : 0; diff --git a/src/phpGPX/Models/Collection.php b/src/phpGPX/Models/Collection.php index 9d2accf..d354fb6 100644 --- a/src/phpGPX/Models/Collection.php +++ b/src/phpGPX/Models/Collection.php @@ -10,7 +10,7 @@ * Class Collection * @package phpGPX\Models */ -abstract class Collection implements \JsonSerializable, StatsCalculator +abstract class Collection implements \JsonSerializable { /** @@ -88,6 +88,7 @@ public function __construct() $this->number = null; $this->type = null; $this->extensions = null; + $this->stats = null; } diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index ea7e1fd..99bfcf2 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -6,9 +6,6 @@ namespace phpGPX\Models; -use phpGPX\Config; -use phpGPX\Helpers\DistanceCalculator; -use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Helpers\SerializationHelper; /** @@ -72,80 +69,4 @@ public function jsonSerialize(): array 'properties' => $properties ?: new \stdClass(), ]; } - - /** - * Recalculate stats objects. - * @return void - */ - public function recalculateStats(Config $config): void - { - if (empty($this->stats)) { - $this->stats = new Stats(); - } - - $this->stats->reset(); - - if (empty($this->points)) { - return; - } - - $pointCount = count($this->points); - - list($this->stats->cumulativeElevationGain, $this->stats->cumulativeElevationLoss) = - ElevationGainLossCalculator::calculate($this->getPoints(), $config); - - $calculator = new DistanceCalculator($this->getPoints(), $config); - $this->stats->distance = $calculator->getRawDistance(); - $this->stats->realDistance = $calculator->getRealDistance(); - - // Find first/last non-null timestamps (#51) - for ($i = 0; $i < $pointCount; $i++) { - if ($this->points[$i]->time instanceof \DateTime) { - $this->stats->startedAt = $this->points[$i]->time; - $this->stats->startedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; - break; - } - } - for ($i = $pointCount - 1; $i >= 0; $i--) { - if ($this->points[$i]->time instanceof \DateTime) { - $this->stats->finishedAt = $this->points[$i]->time; - $this->stats->finishedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; - break; - } - } - - // Find min/max altitude — don't assume first point (#70) - for ($i = 0; $i < $pointCount; $i++) { - $ele = $this->points[$i]->elevation; - if ($ele === null) { - continue; - } - if ($config->ignoreZeroElevation && $ele == 0) { - continue; - } - - $coords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; - - if ($this->stats->maxAltitude === null || $ele > $this->stats->maxAltitude) { - $this->stats->maxAltitude = $ele; - $this->stats->maxAltitudeCoords = $coords; - } - if ($this->stats->minAltitude === null || $ele < $this->stats->minAltitude) { - $this->stats->minAltitude = $ele; - $this->stats->minAltitudeCoords = $coords; - } - } - - if ($this->stats->startedAt instanceof \DateTime && $this->stats->finishedAt instanceof \DateTime) { - $this->stats->duration = $this->stats->finishedAt->getTimestamp() - $this->stats->startedAt->getTimestamp(); - - if ($this->stats->duration != 0) { - $this->stats->averageSpeed = $this->stats->distance / $this->stats->duration; - } - - if ($this->stats->distance != 0) { - $this->stats->averagePace = $this->stats->duration / ($this->stats->distance / 1000); - } - } - } } \ No newline at end of file diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index 97bbecf..5468dc2 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -6,9 +6,6 @@ namespace phpGPX\Models; -use phpGPX\Config; -use phpGPX\Helpers\DistanceCalculator; -use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Helpers\SerializationHelper; /** @@ -18,7 +15,7 @@ * start a new Track Segment for each continuous span of track data. * @package phpGPX\Models */ -class Segment implements \JsonSerializable, StatsCalculator +class Segment implements \JsonSerializable { /** * Array of segment points @@ -71,85 +68,10 @@ public function jsonSerialize(): array } /** - * @return array|Point[] + * @return Point[] */ public function getPoints(): array { return $this->points; } - - /** - * Recalculate stats objects. - * @return void - */ - public function recalculateStats(Config $config): void - { - if (empty($this->stats)) { - $this->stats = new Stats(); - } - - $count = count($this->points); - $this->stats->reset(); - - if (empty($this->points)) { - return; - } - - list($this->stats->cumulativeElevationGain, $this->stats->cumulativeElevationLoss) = - ElevationGainLossCalculator::calculate($this->getPoints(), $config); - - $calculator = new DistanceCalculator($this->getPoints(), $config); - $this->stats->distance = $calculator->getRawDistance(); - $this->stats->realDistance = $calculator->getRealDistance(); - - // Find first/last non-null timestamps (#51) - for ($i = 0; $i < $count; $i++) { - if ($this->points[$i]->time instanceof \DateTime) { - $this->stats->startedAt = $this->points[$i]->time; - $this->stats->startedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; - break; - } - } - for ($i = $count - 1; $i >= 0; $i--) { - if ($this->points[$i]->time instanceof \DateTime) { - $this->stats->finishedAt = $this->points[$i]->time; - $this->stats->finishedAtCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; - break; - } - } - - // Find min/max altitude — don't assume first point (#70) - for ($i = 0; $i < $count; $i++) { - $ele = $this->points[$i]->elevation; - if ($ele === null) { - continue; - } - if ($config->ignoreZeroElevation && $ele == 0) { - continue; - } - - $coords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; - - if ($this->stats->maxAltitude === null || $ele > $this->stats->maxAltitude) { - $this->stats->maxAltitude = $ele; - $this->stats->maxAltitudeCoords = $coords; - } - if ($this->stats->minAltitude === null || $ele < $this->stats->minAltitude) { - $this->stats->minAltitude = $ele; - $this->stats->minAltitudeCoords = $coords; - } - } - - if ($this->stats->startedAt instanceof \DateTime && $this->stats->finishedAt instanceof \DateTime) { - $this->stats->duration = $this->stats->finishedAt->getTimestamp() - $this->stats->startedAt->getTimestamp(); - - if ($this->stats->duration != 0) { - $this->stats->averageSpeed = $this->stats->distance / $this->stats->duration; - } - - if ($this->stats->distance != 0) { - $this->stats->averagePace = $this->stats->duration / ($this->stats->distance / 1000); - } - } - } } \ No newline at end of file diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index 12590c3..960f671 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -105,6 +105,48 @@ class Stats implements \JsonSerializable */ public ?float $duration = null; + /** + * Coordinate bounds + * @var Bounds|null + */ + public ?Bounds $bounds = null; + + /** + * Moving duration in seconds (excludes stopped time) + * @var float|null + */ + public ?float $movingDuration = null; + + /** + * Average speed while moving in meters per second (m/s) + * @var float|null + */ + public ?float $movingAverageSpeed = null; + + /** + * Average heart rate in beats per minute (bpm) + * @var float|null + */ + public ?float $averageHeartRate = null; + + /** + * Maximum heart rate in beats per minute (bpm) + * @var float|null + */ + public ?float $maxHeartRate = null; + + /** + * Average cadence in revolutions per minute (rpm) + * @var float|null + */ + public ?float $averageCadence = null; + + /** + * Average temperature in degrees Celsius + * @var float|null + */ + public ?float $averageTemperature = null; + /** * Reset all stats * @return void @@ -126,6 +168,13 @@ public function reset(): void $this->finishedAt = null; $this->finishedAtCoords = null; $this->duration = null; + $this->bounds = null; + $this->movingDuration = null; + $this->movingAverageSpeed = null; + $this->averageHeartRate = null; + $this->maxHeartRate = null; + $this->averageCadence = null; + $this->averageTemperature = null; } public function jsonSerialize(): array @@ -146,6 +195,13 @@ public function jsonSerialize(): array 'finishedAt' => DateTimeHelper::formatDateTime($this->finishedAt), 'finishedAtCoords' => $this->finishedAtCoords, 'duration' => $this->duration, + 'bounds' => $this->bounds?->jsonSerialize(), + 'movingDuration' => $this->movingDuration, + 'movingAvgSpeed' => $this->movingAverageSpeed, + 'avgHeartRate' => $this->averageHeartRate, + 'maxHeartRate' => $this->maxHeartRate, + 'avgCadence' => $this->averageCadence, + 'avgTemperature' => $this->averageTemperature, ], fn($v) => $v !== null); } } diff --git a/src/phpGPX/Models/StatsCalculator.php b/src/phpGPX/Models/StatsCalculator.php deleted file mode 100644 index 8a756cb..0000000 --- a/src/phpGPX/Models/StatsCalculator.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -namespace phpGPX\Models; - -use phpGPX\Config; - -interface StatsCalculator -{ - - /** - * Recalculate stats objects. - * @param Config $config - * @return void - */ - public function recalculateStats(Config $config): void; - - /** - * Return all points in collection. - * @return Point[] - */ - public function getPoints(): array; -} \ No newline at end of file diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index 1c9067f..99c29e2 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -6,7 +6,6 @@ namespace phpGPX\Models; -use phpGPX\Config; use phpGPX\Helpers\SerializationHelper; /** @@ -80,65 +79,4 @@ public function jsonSerialize(): array 'properties' => $properties ?: new \stdClass(), ]; } - - /** - * Recalculate stats objects. - * @return void - */ - public function recalculateStats(Config $config): void - { - if (empty($this->stats)) { - $this->stats = new Stats(); - } - - $this->stats->reset(); - - if (empty($this->segments)) { - return; - } - - $segmentsCount = count($this->segments); - - for ($s = 0; $s < $segmentsCount; $s++) { - $this->segments[$s]->recalculateStats($config); - $segStats = $this->segments[$s]->stats; - - $this->stats->cumulativeElevationGain += $segStats->cumulativeElevationGain; - $this->stats->cumulativeElevationLoss += $segStats->cumulativeElevationLoss; - $this->stats->distance += $segStats->distance; - $this->stats->realDistance += $segStats->realDistance; - - // Aggregate min/max altitude from segments - if ($segStats->maxAltitude !== null && ($this->stats->maxAltitude === null || $segStats->maxAltitude > $this->stats->maxAltitude)) { - $this->stats->maxAltitude = $segStats->maxAltitude; - $this->stats->maxAltitudeCoords = $segStats->maxAltitudeCoords; - } - if ($segStats->minAltitude !== null && ($this->stats->minAltitude === null || $segStats->minAltitude < $this->stats->minAltitude)) { - $this->stats->minAltitude = $segStats->minAltitude; - $this->stats->minAltitudeCoords = $segStats->minAltitudeCoords; - } - - // Aggregate startedAt/finishedAt from segments (#51) - if ($segStats->startedAt instanceof \DateTime && ($this->stats->startedAt === null || $segStats->startedAt < $this->stats->startedAt)) { - $this->stats->startedAt = $segStats->startedAt; - $this->stats->startedAtCoords = $segStats->startedAtCoords; - } - if ($segStats->finishedAt instanceof \DateTime && ($this->stats->finishedAt === null || $segStats->finishedAt > $this->stats->finishedAt)) { - $this->stats->finishedAt = $segStats->finishedAt; - $this->stats->finishedAtCoords = $segStats->finishedAtCoords; - } - } - - if ($this->stats->startedAt instanceof \DateTime && $this->stats->finishedAt instanceof \DateTime) { - $this->stats->duration = abs($this->stats->finishedAt->getTimestamp() - $this->stats->startedAt->getTimestamp()); - - if ($this->stats->duration != 0) { - $this->stats->averageSpeed = $this->stats->distance / $this->stats->duration; - } - - if ($this->stats->distance != 0) { - $this->stats->averagePace = $this->stats->duration / ($this->stats->distance / 1000); - } - } - } } \ No newline at end of file diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index d5368db..9b1f232 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -6,7 +6,7 @@ namespace phpGPX; -use phpGPX\Helpers\DateTimeHelper; +use phpGPX\Analysis\Engine; use phpGPX\Models\GpxFile; use phpGPX\Parsers\MetadataParser; use phpGPX\Parsers\RouteParser; @@ -28,9 +28,23 @@ class phpGPX public readonly Config $config; - public function __construct(?Config $config = null) + private ?Engine $engine = null; + + public function __construct(?Config $config = null, ?Engine $engine = null) { $this->config = $config ?? new Config(); + $this->engine = $engine; + } + + /** + * Set the stats engine for computing statistics after parsing. + * + * @return $this Fluent interface + */ + public function setEngine(Engine $engine): self + { + $this->engine = $engine; + return $this; } /** @@ -56,42 +70,13 @@ public function parse(string $xml): GpxFile $gpx->tracks = isset($xmlElement->trk) ? TrackParser::parse($xmlElement->trk) : []; $gpx->routes = isset($xmlElement->rte) ? RouteParser::parse($xmlElement->rte) : []; - if ($this->config->sortByTimestamp) { - $this->sortPointsByTimestamp($gpx); - } - - if ($this->config->calculateStats) { - foreach ($gpx->tracks as $track) { - $track->recalculateStats($this->config); - } - foreach ($gpx->routes as $route) { - $route->recalculateStats($this->config); - } + if ($this->engine !== null) { + $gpx = $this->engine->process($gpx); } return $gpx; } - /** - * Sort all point arrays in-place by timestamp. - */ - private function sortPointsByTimestamp(GpxFile $gpx): void - { - foreach ($gpx->tracks as $track) { - foreach ($track->segments as $segment) { - if (!empty($segment->points) && $segment->points[0]->time !== null) { - usort($segment->points, [DateTimeHelper::class, 'comparePointsByTimestamp']); - } - } - } - - foreach ($gpx->routes as $route) { - if (!empty($route->points) && $route->points[0]->time !== null) { - usort($route->points, [DateTimeHelper::class, 'comparePointsByTimestamp']); - } - } - } - /** * Create library signature from name and version. */ diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php index 71d088b..774f47b 100644 --- a/tests/Integration/GeoJsonOutputTest.php +++ b/tests/Integration/GeoJsonOutputTest.php @@ -2,7 +2,6 @@ namespace phpGPX\Tests\Integration; -use phpGPX\Config; use phpGPX\Models\Point; use phpGPX\Models\Route; use phpGPX\Models\Segment; @@ -66,7 +65,6 @@ public function testRouteJsonIsLineStringFeature(): void $p2->elevation = 1.0; $route->points = [$p1, $p2]; - $route->recalculateStats(new Config()); $json = $route->jsonSerialize(); @@ -105,7 +103,6 @@ public function testTrackJsonIsMultiLineStringFeature(): void $seg2->points = [$p3]; $track->segments = [$seg1, $seg2]; - $track->recalculateStats(new Config()); $json = $track->jsonSerialize(); diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php index f755d33..43fc49b 100644 --- a/tests/Integration/GpxFileLoadTest.php +++ b/tests/Integration/GpxFileLoadTest.php @@ -2,6 +2,7 @@ namespace phpGPX\Tests\Integration; +use phpGPX\Analysis\Engine; use phpGPX\phpGPX; use PHPUnit\Framework\TestCase; @@ -13,7 +14,7 @@ class GpxFileLoadTest extends TestCase protected function setUp(): void { - $this->gpx = new phpGPX(); + $this->gpx = new phpGPX(engine: Engine::default()); } public function testLoadTimezeroGpx(): void diff --git a/tests/Integration/XmlRoundTripTest.php b/tests/Integration/XmlRoundTripTest.php index 38d50f9..4bab901 100644 --- a/tests/Integration/XmlRoundTripTest.php +++ b/tests/Integration/XmlRoundTripTest.php @@ -2,6 +2,7 @@ namespace phpGPX\Tests\Integration; +use phpGPX\Analysis\Engine; use phpGPX\phpGPX; use PHPUnit\Framework\TestCase; @@ -13,7 +14,7 @@ class XmlRoundTripTest extends TestCase protected function setUp(): void { - $this->gpx = new phpGPX(); + $this->gpx = new phpGPX(engine: Engine::default()); } /** diff --git a/tests/Unit/Analysis/BoundsAnalyzerTest.php b/tests/Unit/Analysis/BoundsAnalyzerTest.php new file mode 100644 index 0000000..4e94387 --- /dev/null +++ b/tests/Unit/Analysis/BoundsAnalyzerTest.php @@ -0,0 +1,166 @@ +engine = (new Engine())->addAnalyzer(new BoundsAnalyzer()); + } + + private function makePoint(float $lat, float $lon): Point + { + $p = new Point(Point::TRACKPOINT); + $p->latitude = $lat; + $p->longitude = $lon; + return $p; + } + + public function testSegmentBounds(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0), + $this->makePoint(49.0, 18.0), + $this->makePoint(47.5, 16.5), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $bounds = $result->tracks[0]->segments[0]->stats->bounds; + $this->assertNotNull($bounds); + $this->assertEqualsWithDelta(47.5, $bounds->minLatitude, 0.001); + $this->assertEqualsWithDelta(49.0, $bounds->maxLatitude, 0.001); + $this->assertEqualsWithDelta(16.5, $bounds->minLongitude, 0.001); + $this->assertEqualsWithDelta(18.0, $bounds->maxLongitude, 0.001); + } + + public function testTrackBoundsAggregatesSegments(): void + { + $seg1 = new Segment(); + $seg1->points = [ + $this->makePoint(48.0, 17.0), + $this->makePoint(49.0, 18.0), + ]; + + $seg2 = new Segment(); + $seg2->points = [ + $this->makePoint(47.0, 16.0), + $this->makePoint(48.5, 17.5), + ]; + + $track = new Track(); + $track->segments = [$seg1, $seg2]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $trackBounds = $result->tracks[0]->stats->bounds; + $this->assertNotNull($trackBounds); + $this->assertEqualsWithDelta(47.0, $trackBounds->minLatitude, 0.001); + $this->assertEqualsWithDelta(49.0, $trackBounds->maxLatitude, 0.001); + } + + public function testRouteBounds(): void + { + $route = new Route(); + $route->points = [ + $this->makePoint(50.0, 14.0), + $this->makePoint(51.0, 15.0), + ]; + + $gpx = new GpxFile(); + $gpx->routes = [$route]; + + $result = $this->engine->process($gpx); + + $this->assertNotNull($result->routes[0]->stats->bounds); + $this->assertEqualsWithDelta(50.0, $result->routes[0]->stats->bounds->minLatitude, 0.001); + } + + public function testMetadataBoundsSet(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0), + $this->makePoint(49.0, 18.0), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $this->assertNotNull($result->metadata); + $this->assertNotNull($result->metadata->bounds); + $this->assertEqualsWithDelta(48.0, $result->metadata->bounds->minLatitude, 0.001); + } + + public function testEmptyPointsNoBounds(): void + { + $segment = new Segment(); + $segment->points = []; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $this->assertNull($result->tracks[0]->segments[0]->stats->bounds); + } + + public function testMetadataBoundsIncludesWaypoints(): void + { + $waypoint = new Point(Point::WAYPOINT); + $waypoint->latitude = 50.0; + $waypoint->longitude = 20.0; + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + $gpx->waypoints = [$waypoint]; + + $result = $this->engine->process($gpx); + + $bounds = $result->metadata->bounds; + $this->assertNotNull($bounds); + $this->assertEqualsWithDelta(48.0, $bounds->minLatitude, 0.001); + $this->assertEqualsWithDelta(50.0, $bounds->maxLatitude, 0.001); + $this->assertEqualsWithDelta(17.0, $bounds->minLongitude, 0.001); + $this->assertEqualsWithDelta(20.0, $bounds->maxLongitude, 0.001); + } +} \ No newline at end of file diff --git a/tests/Unit/Analysis/EngineTest.php b/tests/Unit/Analysis/EngineTest.php new file mode 100644 index 0000000..3a394f0 --- /dev/null +++ b/tests/Unit/Analysis/EngineTest.php @@ -0,0 +1,323 @@ +latitude = $lat; + $p->longitude = $lon; + $p->elevation = $ele; + $p->time = $time ? new \DateTime($time) : null; + return $p; + } + + public function testAnalyzersCalledInOrder(): void + { + $order = []; + + $a1 = new class($order) extends AbstractPointAnalyzer { + public function __construct(private array &$order) {} + public function begin(): void { $this->order[] = 'a1:begin'; } + public function visit(Point $current, ?Point $previous): void { $this->order[] = 'a1:visit'; } + public function end(Stats $stats): void { $this->order[] = 'a1:end'; } + }; + + $a2 = new class($order) extends AbstractPointAnalyzer { + public function __construct(private array &$order) {} + public function begin(): void { $this->order[] = 'a2:begin'; } + public function visit(Point $current, ?Point $previous): void { $this->order[] = 'a2:visit'; } + public function end(Stats $stats): void { $this->order[] = 'a2:end'; } + }; + + $engine = (new Engine())->addAnalyzer($a1)->addAnalyzer($a2); + + $segment = new Segment(); + $segment->points = [$this->makePoint(48.0, 17.0)]; + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $engine->process($gpx); + + $this->assertSame([ + 'a1:begin', 'a2:begin', // begin phase + 'a1:visit', 'a2:visit', // visit phase (1 point) + 'a1:end', 'a2:end', // end phase + ], $order); + } + + public function testAddAnalyzerReturnsSelf(): void + { + $engine = new Engine(); + $result = $engine->addAnalyzer(new BoundsAnalyzer()); + $this->assertSame($engine, $result); + } + + public function testDefaultFactoryCreatesFullEngine(): void + { + $engine = Engine::default(); + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0, 200, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.001, 210, '2024-01-01T10:00:10Z'), + $this->makePoint(48.002, 17.002, 220, '2024-01-01T10:00:20Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $stats = $result->tracks[0]->segments[0]->stats; + + // Distance + $this->assertGreaterThan(0, $stats->distance); + $this->assertGreaterThan(0, $stats->realDistance); + + // Elevation + $this->assertGreaterThan(0, $stats->cumulativeElevationGain); + + // Altitude + $this->assertEqualsWithDelta(200.0, $stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(220.0, $stats->maxAltitude, 0.01); + + // Timestamps + $this->assertNotNull($stats->startedAt); + $this->assertNotNull($stats->finishedAt); + + // Duration (derived by engine) + $this->assertEqualsWithDelta(20.0, $stats->duration, 0.1); + + // Speed (derived by engine) + $this->assertNotNull($stats->averageSpeed); + + // Bounds + $this->assertNotNull($stats->bounds); + + // Movement + $this->assertNotNull($stats->movingDuration); + + // Metadata bounds (from BoundsAnalyzer::finalizeFile) + $this->assertNotNull($result->metadata); + $this->assertNotNull($result->metadata->bounds); + } + + public function testEngineCreatesStatsForEmptySegment(): void + { + $engine = Engine::default(); + + $segment = new Segment(); + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $this->assertInstanceOf(Stats::class, $result->tracks[0]->segments[0]->stats); + $this->assertNull($result->tracks[0]->segments[0]->stats->distance); + } + + public function testEngineCreatesStatsForEmptyTrack(): void + { + $engine = Engine::default(); + + $track = new Track(); + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $this->assertInstanceOf(Stats::class, $result->tracks[0]->stats); + } + + public function testEngineProcessesRoutes(): void + { + $engine = Engine::default(); + + $route = new Route(); + $route->points = [ + $this->makePoint(48.0, 17.0, 100), + $this->makePoint(48.001, 17.001, 110), + ]; + + $gpx = new GpxFile(); + $gpx->routes = [$route]; + + $result = $engine->process($gpx); + + $this->assertNotNull($result->routes[0]->stats); + $this->assertGreaterThan(0, $result->routes[0]->stats->distance); + } + + public function testDerivedStatsComputedAfterAnalyzers(): void + { + $engine = Engine::default(); + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0, 100, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.001, 110, '2024-01-01T10:01:00Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $stats = $result->tracks[0]->segments[0]->stats; + + // Speed = distance / duration + $expectedSpeed = $stats->distance / $stats->duration; + $this->assertEqualsWithDelta($expectedSpeed, $stats->averageSpeed, 0.001); + + // Pace = duration / (distance / 1000) + $expectedPace = $stats->duration / ($stats->distance / 1000); + $this->assertEqualsWithDelta($expectedPace, $stats->averagePace, 0.001); + } + + public function testNoAnalyzersStillWorks(): void + { + $engine = new Engine(); + + $segment = new Segment(); + $segment->points = [$this->makePoint(48.0, 17.0)]; + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $this->assertInstanceOf(Stats::class, $result->tracks[0]->segments[0]->stats); + } + + public function testSortByTimestampSortsTrackPoints(): void + { + $engine = Engine::default(sortByTimestamp: true); + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.002, 17.0, 100, '2024-01-01T10:00:20Z'), + $this->makePoint(48.000, 17.0, 100, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.0, 100, '2024-01-01T10:00:10Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $points = $result->tracks[0]->segments[0]->points; + $this->assertEqualsWithDelta(48.000, $points[0]->latitude, 0.001); + $this->assertEqualsWithDelta(48.001, $points[1]->latitude, 0.001); + $this->assertEqualsWithDelta(48.002, $points[2]->latitude, 0.001); + } + + public function testSortByTimestampSortsRoutePoints(): void + { + $engine = Engine::default(sortByTimestamp: true); + + $p1 = new Point(Point::ROUTEPOINT); + $p1->latitude = 48.002; + $p1->longitude = 17.0; + $p1->time = new \DateTime('2024-01-01T10:00:20Z'); + + $p2 = new Point(Point::ROUTEPOINT); + $p2->latitude = 48.000; + $p2->longitude = 17.0; + $p2->time = new \DateTime('2024-01-01T10:00:00Z'); + + $route = new Route(); + $route->points = [$p1, $p2]; + $gpx = new GpxFile(); + $gpx->routes = [$route]; + + $result = $engine->process($gpx); + + $this->assertEqualsWithDelta(48.000, $result->routes[0]->points[0]->latitude, 0.001); + $this->assertEqualsWithDelta(48.002, $result->routes[0]->points[1]->latitude, 0.001); + } + + public function testSortByTimestampSkipsPointsWithoutTimestamps(): void + { + $engine = Engine::default(sortByTimestamp: true); + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.002, 17.0, 100), + $this->makePoint(48.000, 17.0, 100), + ]; + + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + // Order should be unchanged + $this->assertEqualsWithDelta(48.002, $result->tracks[0]->segments[0]->points[0]->latitude, 0.001); + $this->assertEqualsWithDelta(48.000, $result->tracks[0]->segments[0]->points[1]->latitude, 0.001); + } + + public function testDefaultFactoryCustomParameters(): void + { + $engine = Engine::default( + applyElevationSmoothing: true, + elevationSmoothingThreshold: 5, + speedThreshold: 1.0, + ); + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0, 100, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.001, 103, '2024-01-01T10:00:10Z'), + $this->makePoint(48.002, 17.002, 120, '2024-01-01T10:00:20Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $stats = $result->tracks[0]->segments[0]->stats; + // With smoothing threshold 5, the 3m gain (100→103) is ignored + // Only the 17m gain (103→120) counts + $this->assertEqualsWithDelta(20.0, $stats->cumulativeElevationGain, 0.01); + } +} \ No newline at end of file diff --git a/tests/Unit/Analysis/MovementAnalyzerTest.php b/tests/Unit/Analysis/MovementAnalyzerTest.php new file mode 100644 index 0000000..e5638fa --- /dev/null +++ b/tests/Unit/Analysis/MovementAnalyzerTest.php @@ -0,0 +1,178 @@ +latitude = $lat; + $p->longitude = $lon; + $p->time = new \DateTime($time); + return $p; + } + + private function makeEngine(float $speedThreshold = 0.1): Engine + { + return (new Engine()) + ->addAnalyzer(new DistanceAnalyzer()) + ->addAnalyzer(new MovementAnalyzer(speedThreshold: $speedThreshold)); + } + + public function testMovingDurationCalculated(): void + { + $engine = $this->makeEngine(speedThreshold: 0.1); + + $segment = new Segment(); + // Points ~111m apart (0.001 degrees lat) with 10s intervals = ~11 m/s + $segment->points = [ + $this->makePoint(48.000, 17.0, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.0, '2024-01-01T10:00:10Z'), + $this->makePoint(48.002, 17.0, '2024-01-01T10:00:20Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $segStats = $result->tracks[0]->segments[0]->stats; + $this->assertNotNull($segStats->movingDuration); + $this->assertEqualsWithDelta(20.0, $segStats->movingDuration, 0.001); + $this->assertNotNull($segStats->movingAverageSpeed); + } + + public function testStoppedPointsExcluded(): void + { + $engine = $this->makeEngine(speedThreshold: 0.5); + + $segment = new Segment(); + // First two points: same location, 60s apart = 0 m/s (stopped) + // Second to third: ~111m, 10s = ~11 m/s (moving) + $segment->points = [ + $this->makePoint(48.000, 17.0, '2024-01-01T10:00:00Z'), + $this->makePoint(48.000, 17.0, '2024-01-01T10:01:00Z'), + $this->makePoint(48.001, 17.0, '2024-01-01T10:01:10Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $segStats = $result->tracks[0]->segments[0]->stats; + // Only the moving segment (10s) should count + $this->assertEqualsWithDelta(10.0, $segStats->movingDuration, 0.001); + } + + public function testTrackAggregatesSegments(): void + { + $engine = $this->makeEngine(speedThreshold: 0.1); + + $seg1 = new Segment(); + $seg1->points = [ + $this->makePoint(48.000, 17.0, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.0, '2024-01-01T10:00:10Z'), + ]; + + $seg2 = new Segment(); + $seg2->points = [ + $this->makePoint(48.002, 17.0, '2024-01-01T11:00:00Z'), + $this->makePoint(48.003, 17.0, '2024-01-01T11:00:20Z'), + ]; + + $track = new Track(); + $track->segments = [$seg1, $seg2]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $trackStats = $result->tracks[0]->stats; + $this->assertEqualsWithDelta(30.0, $trackStats->movingDuration, 0.001); + $this->assertNotNull($trackStats->movingAverageSpeed); + } + + public function testRouteDuration(): void + { + $engine = $this->makeEngine(speedThreshold: 0.1); + + $route = new Route(); + $route->points = [ + $this->makePoint(48.000, 17.0, '2024-01-01T10:00:00Z'), + $this->makePoint(48.001, 17.0, '2024-01-01T10:00:15Z'), + ]; + + $gpx = new GpxFile(); + $gpx->routes = [$route]; + + $result = $engine->process($gpx); + + $this->assertNotNull($result->routes[0]->stats->movingDuration); + $this->assertEqualsWithDelta(15.0, $result->routes[0]->stats->movingDuration, 0.001); + } + + public function testNoTimestampsReturnsNull(): void + { + $engine = $this->makeEngine(); + + $p1 = new Point(Point::TRACKPOINT); + $p1->latitude = 48.0; + $p1->longitude = 17.0; + + $p2 = new Point(Point::TRACKPOINT); + $p2->latitude = 48.1; + $p2->longitude = 17.1; + + $segment = new Segment(); + $segment->points = [$p1, $p2]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $this->assertNull($result->tracks[0]->segments[0]->stats->movingDuration); + } + + public function testSinglePointReturnsNull(): void + { + $engine = $this->makeEngine(); + + $segment = new Segment(); + $segment->points = [ + $this->makePoint(48.0, 17.0, '2024-01-01T10:00:00Z'), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $engine->process($gpx); + + $this->assertNull($result->tracks[0]->segments[0]->stats->movingDuration); + } +} \ No newline at end of file diff --git a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php new file mode 100644 index 0000000..49716bf --- /dev/null +++ b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php @@ -0,0 +1,174 @@ +engine = (new Engine())->addAnalyzer(new TrackPointExtensionAnalyzer()); + } + + private function makePointWithExtension( + float $lat, + float $lon, + ?float $hr = null, + ?float $cad = null, + ?float $aTemp = null + ): Point { + $p = new Point(Point::TRACKPOINT); + $p->latitude = $lat; + $p->longitude = $lon; + + if ($hr !== null || $cad !== null || $aTemp !== null) { + $ext = new TrackPointExtension(); + $ext->hr = $hr; + $ext->cad = $cad; + $ext->aTemp = $aTemp; + + $extensions = new Extensions(); + $extensions->trackPointExtension = $ext; + $p->extensions = $extensions; + } + + return $p; + } + + public function testSegmentHeartRateStats(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePointWithExtension(48.0, 17.0, hr: 120.0), + $this->makePointWithExtension(48.1, 17.1, hr: 140.0), + $this->makePointWithExtension(48.2, 17.2, hr: 160.0), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $segStats = $result->tracks[0]->segments[0]->stats; + $this->assertEqualsWithDelta(140.0, $segStats->averageHeartRate, 0.001); + $this->assertEqualsWithDelta(160.0, $segStats->maxHeartRate, 0.001); + } + + public function testSegmentCadenceStats(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePointWithExtension(48.0, 17.0, cad: 80.0), + $this->makePointWithExtension(48.1, 17.1, cad: 90.0), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $this->assertEqualsWithDelta(85.0, $result->tracks[0]->segments[0]->stats->averageCadence, 0.001); + } + + public function testSegmentTemperatureStats(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePointWithExtension(48.0, 17.0, aTemp: 20.0), + $this->makePointWithExtension(48.1, 17.1, aTemp: 24.0), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $this->assertEqualsWithDelta(22.0, $result->tracks[0]->segments[0]->stats->averageTemperature, 0.001); + } + + public function testTrackAggregatesAcrossSegments(): void + { + $seg1 = new Segment(); + $seg1->points = [ + $this->makePointWithExtension(48.0, 17.0, hr: 100.0), + $this->makePointWithExtension(48.1, 17.1, hr: 120.0), + ]; + + $seg2 = new Segment(); + $seg2->points = [ + $this->makePointWithExtension(48.2, 17.2, hr: 140.0), + $this->makePointWithExtension(48.3, 17.3, hr: 160.0), + ]; + + $track = new Track(); + $track->segments = [$seg1, $seg2]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $trackStats = $result->tracks[0]->stats; + // Weighted average: (100+120+140+160)/4 = 130 + $this->assertEqualsWithDelta(130.0, $trackStats->averageHeartRate, 0.001); + $this->assertEqualsWithDelta(160.0, $trackStats->maxHeartRate, 0.001); + } + + public function testNoExtensionDataLeavesStatsNull(): void + { + $segment = new Segment(); + $segment->points = [ + $this->makePointWithExtension(48.0, 17.0), + $this->makePointWithExtension(48.1, 17.1), + ]; + + $track = new Track(); + $track->segments = [$segment]; + + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + + $result = $this->engine->process($gpx); + + $this->assertNull($result->tracks[0]->segments[0]->stats->averageHeartRate); + $this->assertNull($result->tracks[0]->segments[0]->stats->averageCadence); + $this->assertNull($result->tracks[0]->segments[0]->stats->averageTemperature); + } + + public function testRouteExtensionStats(): void + { + $route = new Route(); + $route->points = [ + $this->makePointWithExtension(48.0, 17.0, hr: 130.0), + $this->makePointWithExtension(48.1, 17.1, hr: 150.0), + ]; + + $gpx = new GpxFile(); + $gpx->routes = [$route]; + + $result = $this->engine->process($gpx); + + $this->assertEqualsWithDelta(140.0, $result->routes[0]->stats->averageHeartRate, 0.001); + } +} \ No newline at end of file diff --git a/tests/Unit/Helpers/DateTimeHelperTest.php b/tests/Unit/Helpers/DateTimeHelperTest.php index ea53278..72a416c 100644 --- a/tests/Unit/Helpers/DateTimeHelperTest.php +++ b/tests/Unit/Helpers/DateTimeHelperTest.php @@ -3,24 +3,10 @@ namespace phpGPX\Tests\Unit\Helpers; use phpGPX\Helpers\DateTimeHelper; -use phpGPX\Models\Point; use PHPUnit\Framework\TestCase; class DateTimeHelperTest extends TestCase { - public function testComparePointsByTimestamp(): void - { - $point1 = new Point(Point::WAYPOINT); - $time1 = new \DateTime("2017-08-12T20:16:29+00:00", new \DateTimeZone("UTC")); - $point1->time = $time1; - - $point2 = new Point(Point::WAYPOINT); - $time2 = new \DateTime("2017-08-12T20:15:19+00:00", new \DateTimeZone("UTC")); - $point2->time = $time2; - - $this->assertTrue(($time1 > $time2) && DateTimeHelper::comparePointsByTimestamp($point1, $point2)); - } - public function testFormatDateTime(): void { $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); diff --git a/tests/Unit/Helpers/DistanceCalculatorTest.php b/tests/Unit/Helpers/DistanceCalculatorTest.php index 7235ff3..10e8b7c 100644 --- a/tests/Unit/Helpers/DistanceCalculatorTest.php +++ b/tests/Unit/Helpers/DistanceCalculatorTest.php @@ -2,7 +2,6 @@ namespace phpGPX\Tests\Unit\Helpers; -use phpGPX\Config; use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; @@ -21,7 +20,7 @@ private function makePoint(float $lat, float $lon, ?float $ele = null): Point public function testEmptyPoints(): void { - $calc = new DistanceCalculator([], new Config()); + $calc = new DistanceCalculator([]); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); $this->assertEqualsWithDelta(0.0, $calc->getRealDistance(), 0.001); } @@ -29,7 +28,7 @@ public function testEmptyPoints(): void public function testSinglePoint(): void { $points = [$this->makePoint(48.157, 17.054)]; - $calc = new DistanceCalculator($points, new Config()); + $calc = new DistanceCalculator($points); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); } @@ -41,7 +40,7 @@ public function testTwoPoints(): void $expectedRaw = GeoHelper::getRawDistance($p1, $p2); $expectedReal = GeoHelper::getRealDistance($p1, $p2); - $calc = new DistanceCalculator([$p1, $p2], new Config()); + $calc = new DistanceCalculator([$p1, $p2]); $this->assertEqualsWithDelta($expectedRaw, $calc->getRawDistance(), 0.01); $this->assertEqualsWithDelta($expectedReal, $calc->getRealDistance(), 0.01); @@ -57,7 +56,7 @@ public function testMultiplePointsAccumulate(): void $d12 = GeoHelper::getRawDistance($p1, $p2); $d23 = GeoHelper::getRawDistance($p2, $p3); - $calc = new DistanceCalculator([$p1, $p2, $p3], new Config()); + $calc = new DistanceCalculator([$p1, $p2, $p3]); $totalRaw = $calc->getRawDistance(); $this->assertEqualsWithDelta($d12 + $d23, $totalRaw, 0.01); @@ -69,7 +68,7 @@ public function testPointsDifferenceAndDistanceAreSet(): void $p2 = $this->makePoint(46.572016, 8.414866); $p3 = $this->makePoint(46.572088, 8.414911); - $calc = new DistanceCalculator([$p1, $p2, $p3], new Config()); + $calc = new DistanceCalculator([$p1, $p2, $p3]); $calc->getRawDistance(); // First point should have no difference set @@ -86,17 +85,12 @@ public function testPointsDifferenceAndDistanceAreSet(): void public function testDistanceSmoothingFiltersSmallMovements(): void { - $config = new Config( - applyDistanceSmoothing: true, - distanceSmoothingThreshold: 10, - ); - // Points very close together (< 10m apart) $p1 = $this->makePoint(46.571948, 8.414757); $p2 = $this->makePoint(46.571949, 8.414758); // ~0.1m away $p3 = $this->makePoint(46.571950, 8.414759); // ~0.1m away - $calc = new DistanceCalculator([$p1, $p2, $p3], $config); + $calc = new DistanceCalculator([$p1, $p2, $p3], applySmoothing: true, smoothingThreshold: 10); $distance = $calc->getRawDistance(); // With smoothing, these tiny movements should be filtered out @@ -105,16 +99,11 @@ public function testDistanceSmoothingFiltersSmallMovements(): void public function testDistanceSmoothingKeepsLargeMovements(): void { - $config = new Config( - applyDistanceSmoothing: true, - distanceSmoothingThreshold: 2, - ); - // Points ~857m apart — well above threshold $p1 = $this->makePoint(48.1573923225717, 17.0547121910204); $p2 = $this->makePoint(48.1644916381763, 17.0591753907502); - $calc = new DistanceCalculator([$p1, $p2], $config); + $calc = new DistanceCalculator([$p1, $p2], applySmoothing: true, smoothingThreshold: 2); $distance = $calc->getRawDistance(); $this->assertGreaterThan(800, $distance); @@ -126,7 +115,7 @@ public function testSamePointRepeatedZeroDistance(): void $p2 = $this->makePoint(46.571948, 8.414757); $p3 = $this->makePoint(46.571948, 8.414757); - $calc = new DistanceCalculator([$p1, $p2, $p3], new Config()); + $calc = new DistanceCalculator([$p1, $p2, $p3]); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); } } \ No newline at end of file diff --git a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php index 4afc9d7..11c7a92 100644 --- a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php +++ b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php @@ -2,7 +2,6 @@ namespace phpGPX\Tests\Unit\Helpers; -use phpGPX\Config; use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Models\Point; use PHPUnit\Framework\TestCase; @@ -20,14 +19,14 @@ private function makePoint(float $ele): Point public function testEmptyPoints(): void { - [$gain, $loss] = ElevationGainLossCalculator::calculate([], new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate([]); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testSinglePoint(): void { - [$gain, $loss] = ElevationGainLossCalculator::calculate([$this->makePoint(100)], new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate([$this->makePoint(100)]); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } @@ -40,7 +39,7 @@ public function testFlatTrack(): void $this->makePoint(100), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } @@ -53,7 +52,7 @@ public function testUphillOnly(): void $this->makePoint(200), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); $this->assertEqualsWithDelta(100.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } @@ -66,7 +65,7 @@ public function testDownhillOnly(): void $this->makePoint(100), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(100.0, $loss, 0.001); } @@ -81,7 +80,7 @@ public function testUpAndDown(): void $this->makePoint(140), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points); $this->assertEqualsWithDelta(70.0, $gain, 0.001); // 50 + 20 $this->assertEqualsWithDelta(30.0, $loss, 0.001); } @@ -95,48 +94,39 @@ public function testNullElevationSkipped(): void $p2->elevation = null; $p3 = $this->makePoint(200); - [$gain, $loss] = ElevationGainLossCalculator::calculate([$p1, $p2, $p3], new Config()); + [$gain, $loss] = ElevationGainLossCalculator::calculate([$p1, $p2, $p3]); $this->assertEqualsWithDelta(100.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testIgnoreElevationZero(): void { - $config = new Config(ignoreZeroElevation: true); - $points = [ $this->makePoint(100), $this->makePoint(0), // should be skipped $this->makePoint(200), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, ignoreZeroElevation: true); $this->assertEqualsWithDelta(100.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testIgnoreElevationZeroDisabled(): void { - $config = new Config(ignoreZeroElevation: false); - $points = [ $this->makePoint(100), $this->makePoint(0), $this->makePoint(200), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); + [$gain, $loss] = ElevationGainLossCalculator::calculate($points, ignoreZeroElevation: false); $this->assertEqualsWithDelta(200.0, $gain, 0.001); // 0→200 $this->assertEqualsWithDelta(100.0, $loss, 0.001); // 100→0 } public function testSmoothingFiltersSmallChanges(): void { - $config = new Config( - applyElevationSmoothing: true, - elevationSmoothingThreshold: 5, - ); - // Small oscillations of 2m — below 5m threshold, should be filtered $points = [ $this->makePoint(100), @@ -146,37 +136,34 @@ public function testSmoothingFiltersSmallChanges(): void $this->makePoint(100), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); + [$gain, $loss] = ElevationGainLossCalculator::calculate( + $points, + applySmoothing: true, + smoothingThreshold: 5, + ); $this->assertEqualsWithDelta(0.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testSmoothingKeepsLargeChanges(): void { - $config = new Config( - applyElevationSmoothing: true, - elevationSmoothingThreshold: 5, - ); - // Large change of 50m — above 5m threshold $points = [ $this->makePoint(100), $this->makePoint(150), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); + [$gain, $loss] = ElevationGainLossCalculator::calculate( + $points, + applySmoothing: true, + smoothingThreshold: 5, + ); $this->assertEqualsWithDelta(50.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } public function testSmoothingSpikesThreshold(): void { - $config = new Config( - applyElevationSmoothing: true, - elevationSmoothingThreshold: 2, - elevationSmoothingSpikesThreshold: 50, - ); - // Spike of 100m — above spikes threshold, should be filtered $points = [ $this->makePoint(100), @@ -184,7 +171,12 @@ public function testSmoothingSpikesThreshold(): void $this->makePoint(105), ]; - [$gain, $loss] = ElevationGainLossCalculator::calculate($points, $config); + [$gain, $loss] = ElevationGainLossCalculator::calculate( + $points, + applySmoothing: true, + smoothingThreshold: 2, + spikesThreshold: 50, + ); // The 100m jump is filtered (> spikes threshold) // The 200→105 drop: delta from last considered (100) to 200 is 100 (filtered) // delta from 100 to 105 is 5 (above 2, below 50) — counted diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php index 653743c..cbf616e 100644 --- a/tests/Unit/Models/StatsCalculationTest.php +++ b/tests/Unit/Models/StatsCalculationTest.php @@ -2,7 +2,12 @@ namespace phpGPX\Tests\Unit\Models; -use phpGPX\Config; +use phpGPX\Analysis\AltitudeAnalyzer; +use phpGPX\Analysis\DistanceAnalyzer; +use phpGPX\Analysis\ElevationAnalyzer; +use phpGPX\Analysis\Engine; +use phpGPX\Analysis\TimestampAnalyzer; +use phpGPX\Models\GpxFile; use phpGPX\Models\Point; use phpGPX\Models\Route; use phpGPX\Models\Segment; @@ -12,11 +17,15 @@ class StatsCalculationTest extends TestCase { - private Config $config; + private Engine $engine; protected function setUp(): void { - $this->config = new Config(); + $this->engine = (new Engine()) + ->addAnalyzer(new DistanceAnalyzer()) + ->addAnalyzer(new ElevationAnalyzer()) + ->addAnalyzer(new AltitudeAnalyzer()) + ->addAnalyzer(new TimestampAnalyzer()); } private function makePoint( @@ -47,15 +56,37 @@ private function makeRoutePoint( return $p; } + private function processTrack(Track $track): GpxFile + { + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + return $this->engine->process($gpx); + } + + private function processRoute(Route $route): GpxFile + { + $gpx = new GpxFile(); + $gpx->routes = [$route]; + return $this->engine->process($gpx); + } + + private function processSegment(Segment $segment): GpxFile + { + $track = new Track(); + $track->segments = [$segment]; + return $this->processTrack($track); + } + // --- Segment stats --- public function testSegmentStatsEmptyPoints(): void { $segment = new Segment(); - $segment->recalculateStats($this->config); + $result = $this->processSegment($segment); - $this->assertInstanceOf(Stats::class, $segment->stats); - $this->assertNull($segment->stats->distance); + $stats = $result->tracks[0]->segments[0]->stats; + $this->assertInstanceOf(Stats::class, $stats); + $this->assertNull($stats->distance); } public function testSegmentStatsSinglePoint(): void @@ -64,13 +95,14 @@ public function testSegmentStatsSinglePoint(): void $segment->points = [ $this->makePoint(46.571948, 8.414757, 2419, '2017-08-13T07:10:41Z'), ]; - $segment->recalculateStats($this->config); - - $this->assertEqualsWithDelta(0.0, $segment->stats->distance, 0.01); - $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationGain, 0.01); - $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationLoss, 0.01); - $this->assertEquals(46.571948, $segment->stats->startedAtCoords['lat']); - $this->assertEquals(46.571948, $segment->stats->finishedAtCoords['lat']); + $result = $this->processSegment($segment); + $stats = $result->tracks[0]->segments[0]->stats; + + $this->assertEqualsWithDelta(0.0, $stats->distance, 0.01); + $this->assertEqualsWithDelta(0.0, $stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(0.0, $stats->cumulativeElevationLoss, 0.01); + $this->assertEquals(46.571948, $stats->startedAtCoords['lat']); + $this->assertEquals(46.571948, $stats->finishedAtCoords['lat']); } public function testSegmentStatsBasicTrack(): void @@ -83,34 +115,35 @@ public function testSegmentStatsBasicTrack(): void $this->makePoint(46.572069, 8.414912, 2422, '2017-08-13T07:12:15Z'), $this->makePoint(46.572054, 8.414888, 2425, '2017-08-13T07:12:18Z'), ]; - $segment->recalculateStats($this->config); + $result = $this->processSegment($segment); + $stats = $result->tracks[0]->segments[0]->stats; // Distance should be positive - $this->assertGreaterThan(0, $segment->stats->distance); - $this->assertGreaterThan(0, $segment->stats->realDistance); + $this->assertGreaterThan(0, $stats->distance); + $this->assertGreaterThan(0, $stats->realDistance); // Elevation gain: 2418.88→2419.90 (+1.02), 2419.90→2422 (+2.1), 2422→2425 (+3) - $this->assertGreaterThan(6, $segment->stats->cumulativeElevationGain); + $this->assertGreaterThan(6, $stats->cumulativeElevationGain); // Elevation loss: 2419→2418.88 (-0.12) - $this->assertGreaterThan(0, $segment->stats->cumulativeElevationLoss); + $this->assertGreaterThan(0, $stats->cumulativeElevationLoss); // Duration - $this->assertEqualsWithDelta(97.0, $segment->stats->duration, 0.1); + $this->assertEqualsWithDelta(97.0, $stats->duration, 0.1); // Speed and pace - $this->assertNotNull($segment->stats->averageSpeed); - $this->assertGreaterThan(0, $segment->stats->averageSpeed); - $this->assertNotNull($segment->stats->averagePace); - $this->assertGreaterThan(0, $segment->stats->averagePace); + $this->assertNotNull($stats->averageSpeed); + $this->assertGreaterThan(0, $stats->averageSpeed); + $this->assertNotNull($stats->averagePace); + $this->assertGreaterThan(0, $stats->averagePace); // Altitude bounds - $this->assertEqualsWithDelta(2418.88, $segment->stats->minAltitude, 0.01); - $this->assertEqualsWithDelta(2425, $segment->stats->maxAltitude, 0.01); + $this->assertEqualsWithDelta(2418.88, $stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(2425, $stats->maxAltitude, 0.01); // Start/end coordinates - $this->assertEquals(46.571948, $segment->stats->startedAtCoords['lat']); - $this->assertEquals(46.572054, $segment->stats->finishedAtCoords['lat']); + $this->assertEquals(46.571948, $stats->startedAtCoords['lat']); + $this->assertEquals(46.572054, $stats->finishedAtCoords['lat']); } public function testSegmentStatsWithoutTimestamps(): void @@ -120,15 +153,16 @@ public function testSegmentStatsWithoutTimestamps(): void $this->makePoint(46.571948, 8.414757, 100), $this->makePoint(46.572016, 8.414866, 200), ]; - $segment->recalculateStats($this->config); + $result = $this->processSegment($segment); + $stats = $result->tracks[0]->segments[0]->stats; // Distance should still be calculated - $this->assertGreaterThan(0, $segment->stats->distance); + $this->assertGreaterThan(0, $stats->distance); // Duration, speed, pace should be null (no timestamps) - $this->assertNull($segment->stats->duration); - $this->assertNull($segment->stats->averageSpeed); - $this->assertNull($segment->stats->averagePace); + $this->assertNull($stats->duration); + $this->assertNull($stats->averageSpeed); + $this->assertNull($stats->averagePace); } public function testSegmentStatsWithoutElevation(): void @@ -138,16 +172,21 @@ public function testSegmentStatsWithoutElevation(): void $this->makePoint(46.571948, 8.414757, null, '2017-08-13T07:10:41Z'), $this->makePoint(46.572016, 8.414866, null, '2017-08-13T07:10:54Z'), ]; - $segment->recalculateStats($this->config); + $result = $this->processSegment($segment); + $stats = $result->tracks[0]->segments[0]->stats; - $this->assertGreaterThan(0, $segment->stats->distance); - $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationGain, 0.001); - $this->assertEqualsWithDelta(0.0, $segment->stats->cumulativeElevationLoss, 0.001); + $this->assertGreaterThan(0, $stats->distance); + $this->assertEqualsWithDelta(0.0, $stats->cumulativeElevationGain, 0.001); + $this->assertEqualsWithDelta(0.0, $stats->cumulativeElevationLoss, 0.001); } public function testSegmentStatsIgnoreElevationZero(): void { - $config = new Config(ignoreZeroElevation: true); + $engine = (new Engine()) + ->addAnalyzer(new DistanceAnalyzer()) + ->addAnalyzer(new ElevationAnalyzer(ignoreZeroElevation: true)) + ->addAnalyzer(new AltitudeAnalyzer(ignoreZeroElevation: true)) + ->addAnalyzer(new TimestampAnalyzer()); $segment = new Segment(); $segment->points = [ @@ -155,10 +194,15 @@ public function testSegmentStatsIgnoreElevationZero(): void $this->makePoint(46.572016, 8.414866, 0, '2017-08-13T07:10:54Z'), $this->makePoint(46.572088, 8.414911, 200, '2017-08-13T07:11:56Z'), ]; - $segment->recalculateStats($config); + + $track = new Track(); + $track->segments = [$segment]; + $gpx = new GpxFile(); + $gpx->tracks = [$track]; + $result = $engine->process($gpx); // minAltitude should NOT be 0 when ignoreZeroElevation is true - $this->assertGreaterThan(0, $segment->stats->minAltitude); + $this->assertGreaterThan(0, $result->tracks[0]->segments[0]->stats->minAltitude); } public function testSegmentStatsRecalculateResetsValues(): void @@ -168,12 +212,12 @@ public function testSegmentStatsRecalculateResetsValues(): void $this->makePoint(46.571948, 8.414757, 100, '2017-08-13T07:10:41Z'), $this->makePoint(46.572016, 8.414866, 200, '2017-08-13T07:10:54Z'), ]; - $segment->recalculateStats($this->config); - $firstDistance = $segment->stats->distance; + $result = $this->processSegment($segment); + $firstDistance = $result->tracks[0]->segments[0]->stats->distance; - // Recalculate again — should get same result (not accumulated) - $segment->recalculateStats($this->config); - $this->assertEqualsWithDelta($firstDistance, $segment->stats->distance, 0.001); + // Process again — should get same result (not accumulated) + $result2 = $this->processSegment($segment); + $this->assertEqualsWithDelta($firstDistance, $result2->tracks[0]->segments[0]->stats->distance, 0.001); } // --- Track stats --- @@ -181,10 +225,10 @@ public function testSegmentStatsRecalculateResetsValues(): void public function testTrackStatsEmptySegments(): void { $track = new Track(); - $track->recalculateStats($this->config); + $result = $this->processTrack($track); - $this->assertInstanceOf(Stats::class, $track->stats); - $this->assertNull($track->stats->distance); + $this->assertInstanceOf(Stats::class, $result->tracks[0]->stats); + $this->assertNull($result->tracks[0]->stats->distance); } public function testTrackStatsSingleSegment(): void @@ -197,12 +241,13 @@ public function testTrackStatsSingleSegment(): void $track = new Track(); $track->segments = [$segment]; - $track->recalculateStats($this->config); + $result = $this->processTrack($track); + $stats = $result->tracks[0]->stats; - $this->assertGreaterThan(0, $track->stats->distance); - $this->assertEqualsWithDelta(6.0, $track->stats->cumulativeElevationGain, 0.01); - $this->assertEqualsWithDelta(2419.0, $track->stats->minAltitude, 0.01); - $this->assertEqualsWithDelta(2425.0, $track->stats->maxAltitude, 0.01); + $this->assertGreaterThan(0, $stats->distance); + $this->assertEqualsWithDelta(6.0, $stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(2419.0, $stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(2425.0, $stats->maxAltitude, 0.01); } public function testTrackStatsMultipleSegmentsAggregated(): void @@ -221,32 +266,33 @@ public function testTrackStatsMultipleSegmentsAggregated(): void $track = new Track(); $track->segments = [$seg1, $seg2]; - $track->recalculateStats($this->config); + $result = $this->processTrack($track); + $stats = $result->tracks[0]->stats; - // Distances should be summed across segments - $seg1->recalculateStats($this->config); - $seg2->recalculateStats($this->config); - $expectedDistance = $seg1->stats->distance + $seg2->stats->distance; - $this->assertEqualsWithDelta($expectedDistance, $track->stats->distance, 0.01); + // Get individual segment distances for comparison + $seg1Distance = $result->tracks[0]->segments[0]->stats->distance; + $seg2Distance = $result->tracks[0]->segments[1]->stats->distance; + $expectedDistance = $seg1Distance + $seg2Distance; + $this->assertEqualsWithDelta($expectedDistance, $stats->distance, 0.01); // Elevation gain aggregated: seg1 has 50m gain, seg2 has 0 - $this->assertEqualsWithDelta(50.0, $track->stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(50.0, $stats->cumulativeElevationGain, 0.01); // Elevation loss aggregated: seg1 has 0, seg2 has 20m loss - $this->assertEqualsWithDelta(20.0, $track->stats->cumulativeElevationLoss, 0.01); + $this->assertEqualsWithDelta(20.0, $stats->cumulativeElevationLoss, 0.01); // Min altitude should be minimum across all segments - $this->assertEqualsWithDelta(100.0, $track->stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(100.0, $stats->minAltitude, 0.01); // Max altitude should be maximum across all segments - $this->assertEqualsWithDelta(200.0, $track->stats->maxAltitude, 0.01); + $this->assertEqualsWithDelta(200.0, $stats->maxAltitude, 0.01); // Start/end should span the entire track - $this->assertNotNull($track->stats->startedAt); - $this->assertNotNull($track->stats->finishedAt); + $this->assertNotNull($stats->startedAt); + $this->assertNotNull($stats->finishedAt); // Duration spans first point of first segment to last point of last segment - $this->assertEqualsWithDelta(330.0, $track->stats->duration, 0.1); + $this->assertEqualsWithDelta(330.0, $stats->duration, 0.1); } public function testTrackGetPointsFlattensSegments(): void @@ -274,10 +320,10 @@ public function testTrackGetPointsFlattensSegments(): void public function testRouteStatsEmptyPoints(): void { $route = new Route(); - $route->recalculateStats($this->config); + $result = $this->processRoute($route); - $this->assertInstanceOf(Stats::class, $route->stats); - $this->assertNull($route->stats->distance); + $this->assertInstanceOf(Stats::class, $result->routes[0]->stats); + $this->assertNull($result->routes[0]->stats->distance); } public function testRouteStatsBasic(): void @@ -289,13 +335,14 @@ public function testRouteStatsBasic(): void $this->makeRoutePoint(54.93327743521187, 9.86187816543752, 2.0), $this->makeRoutePoint(54.93342326167919, 9.862439849679859, 3.0), ]; - $route->recalculateStats($this->config); - - $this->assertGreaterThan(0, $route->stats->distance); - $this->assertEqualsWithDelta(3.0, $route->stats->cumulativeElevationGain, 0.01); - $this->assertEqualsWithDelta(0.0, $route->stats->cumulativeElevationLoss, 0.01); - $this->assertEqualsWithDelta(0.0, $route->stats->minAltitude, 0.01); - $this->assertEqualsWithDelta(3.0, $route->stats->maxAltitude, 0.01); + $result = $this->processRoute($route); + $stats = $result->routes[0]->stats; + + $this->assertGreaterThan(0, $stats->distance); + $this->assertEqualsWithDelta(3.0, $stats->cumulativeElevationGain, 0.01); + $this->assertEqualsWithDelta(0.0, $stats->cumulativeElevationLoss, 0.01); + $this->assertEqualsWithDelta(0.0, $stats->minAltitude, 0.01); + $this->assertEqualsWithDelta(3.0, $stats->maxAltitude, 0.01); } // --- Stats model --- From 9a4302864c87a15bafb912a9ae60ba6019a2912b Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 23:23:39 +0100 Subject: [PATCH 20/31] =?UTF-8?q?Refactored=20Extensions=20framework=20?= =?UTF-8?q?=F0=9F=AB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 112 ++------- docs/00_Getting_Started/02_Quick_Start.md | 2 +- docs/01_Usage/01_Loading_Files.md | 2 +- docs/01_Usage/02_Creating_Files.md | 28 +++ docs/01_Usage/03_Statistics.md | 6 +- docs/01_Usage/04_Configuration.md | 19 +- docs/01_Usage/05_Extensions.md | 140 ++++++++++- docs/04_Development/04_Stats_Architecture.md | 235 ++++++++++++++++++ docs/index.md | 8 +- examples/CreateFileFromScratch.php | 2 +- .../Analysis/TrackPointExtensionAnalyzer.php | 3 +- src/phpGPX/Models/Extensions.php | 101 ++++++-- .../Models/Extensions/AbstractExtension.php | 25 +- .../Models/Extensions/ExtensionInterface.php | 57 +++++ .../Models/Extensions/TrackPointExtension.php | 106 +++----- src/phpGPX/Models/GpxFile.php | 8 +- src/phpGPX/Parsers/AbstractParser.php | 5 + src/phpGPX/Parsers/ExtensionParser.php | 83 ++++--- src/phpGPX/Parsers/ExtensionRegistry.php | 98 ++++++++ .../Extensions/ExtensionParserInterface.php | 47 ++++ .../Extensions/TrackPointExtensionParser.php | 28 +-- src/phpGPX/Parsers/SegmentParser.php | 2 +- src/phpGPX/phpGPX.php | 32 ++- .../Fixtures/Parsers/Extension/extension.json | 2 +- tests/Integration/GpxFileLoadTest.php | 17 +- tests/Integration/XmlRoundTripTest.php | 6 +- .../TrackPointExtensionAnalyzerTest.php | 2 +- tests/Unit/Parsers/ExtensionParserTest.php | 14 +- tests/Unit/Parsers/ExtensionRegistryTest.php | 99 ++++++++ 29 files changed, 993 insertions(+), 296 deletions(-) create mode 100644 docs/04_Development/04_Stats_Architecture.md create mode 100644 src/phpGPX/Models/Extensions/ExtensionInterface.php create mode 100644 src/phpGPX/Parsers/ExtensionRegistry.php create mode 100644 src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php create mode 100644 tests/Unit/Parsers/ExtensionRegistryTest.php diff --git a/README.md b/README.md index 9c32f4f..f7a59fd 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,15 @@ Repository branches: - Extensions support. - JSON (GeoJSON) & XML output. -### Supported Extensions +### Extension Registry -- Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd): - http://www.garmin.com/xmlschemas/TrackPointExtension/v1 +Built-in support for Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd) (v1 + v2). +Custom extensions can be registered via `ExtensionInterface` + `ExtensionParserInterface`: + +```php +$gpx = new phpGPX(); +$gpx->registerExtension('http://example.com/ext/v1', MyExtensionParser::class, 'myext'); +``` ### Stats calculation @@ -94,106 +99,41 @@ $file->save('output.json', phpGPX::JSON_FORMAT); 9.860624216140083, - 'latitude' => 54.9328621088893, - 'elevation' => 0, - 'time' => new \DateTime("+ 1 MINUTE") - ], - [ - 'latitude' => 54.83293237320851, - 'longitude' => 9.76092208681491, - 'elevation' => 10.0, - 'time' => new \DateTime("+ 2 MINUTE") - ], - [ - 'latitude' => 54.73327743521187, - 'longitude' => 9.66187816543752, - 'elevation' => 42.42, - 'time' => new \DateTime("+ 3 MINUTE") - ], - [ - 'latitude' => 54.63342326167919, - 'longitude' => 9.562439849679859, - 'elevation' => 12, - 'time' => new \DateTime("+ 4 MINUTE") - ] -]; - -// Creating sample link object for metadata -$link = new Link(); -$link->href = "https://sibyx.github.io/phpgpx"; -$link->text = 'phpGPX Docs'; - -// GpxFile contains data and handles serialization of objects $gpx_file = new GpxFile(); -// Creating sample Metadata object -$gpx_file->metadata = new Metadata(); - -// Time attribute is always \DateTime object! -$gpx_file->metadata->time = new \DateTime(); - -// Description of GPX file -$gpx_file->metadata->description = "My pretty awesome GPX file, created using phpGPX library!"; - -// Adding link created before to links array of metadata -// Metadata of GPX file can contain more than one link -$gpx_file->metadata->links[] = $link; - -// Creating track $track = new Track(); - -// Name of track -$track->name = "Some random points in logical order. Input array should be already ordered!"; - -// Type of data stored in track +$track->name = "Morning run"; $track->type = 'RUN'; -// Source of GPS coordinates -$track->source = "MySpecificGarminDevice"; - -// Creating Track segment $segment = new Segment(); -foreach ($sample_data as $sample_point) { - // Creating trackpoint - $point = new Point(Point::TRACKPOINT); - $point->latitude = $sample_point['latitude']; - $point->longitude = $sample_point['longitude']; - $point->elevation = $sample_point['elevation']; - $point->time = $sample_point['time']; +$point = new Point(Point::TRACKPOINT); +$point->latitude = 54.9328621088893; +$point->longitude = 9.860624216140083; +$point->elevation = 0; +$point->time = new \DateTime("2024-01-15T07:00:00Z"); - $segment->points[] = $point; -} +// Add extension data +$point->extensions = new Extensions(); +$ext = new TrackPointExtension(); +$ext->hr = 145.0; +$point->extensions->set($ext); -// Add segment to segment array of track +$segment->points[] = $point; $track->segments[] = $segment; - -// Add track to file $gpx_file->tracks[] = $track; -// GPX output -$gpx_file->save('CreatingFileFromScratchExample.gpx', \phpGPX\phpGPX::XML_FORMAT); - -// Serialized data as JSON (GeoJSON) -$gpx_file->save('CreatingFileFromScratchExample.json', \phpGPX\phpGPX::JSON_FORMAT); - -// Direct GPX output to browser -header("Content-Type: application/gpx+xml"); -header("Content-Disposition: attachment; filename=CreatingFileFromScratchExample.gpx"); +// Save as GPX XML +$gpx_file->save('output.gpx', \phpGPX\phpGPX::XML_FORMAT); -echo $gpx_file->toXML()->saveXML(); -exit(); +// Save as GeoJSON +$gpx_file->save('output.json', \phpGPX\phpGPX::JSON_FORMAT); ``` Currently supported output formats: diff --git a/docs/00_Getting_Started/02_Quick_Start.md b/docs/00_Getting_Started/02_Quick_Start.md index 5887d83..4796847 100644 --- a/docs/00_Getting_Started/02_Quick_Start.md +++ b/docs/00_Getting_Started/02_Quick_Start.md @@ -14,7 +14,7 @@ $file = $gpx->load('path/to/file.gpx'); You can also parse GPX data from a string: ```php -$gpx = new phpGPX(engine: engine::default()); +$gpx = new phpGPX(engine: Engine::default()); $xml = file_get_contents('path/to/file.gpx'); $file = $gpx->parse($xml); diff --git a/docs/01_Usage/01_Loading_Files.md b/docs/01_Usage/01_Loading_Files.md index 7719b4a..2735640 100644 --- a/docs/01_Usage/01_Loading_Files.md +++ b/docs/01_Usage/01_Loading_Files.md @@ -66,7 +66,7 @@ When loading a GPX file, phpGPX processes: - **Waypoints** (``) - individual points with coordinates, elevation, time, and all optional GPX 1.1 attributes - **Tracks** (``) - containing segments (``) of track points (``) - **Routes** (``) - containing route points (``) -- **Extensions** - Garmin TrackPointExtension (heart rate, temperature, cadence) and unsupported extensions preserved as key-value pairs +- **Extensions** - Parsed via the extension registry. Garmin TrackPointExtension (heart rate, temperature, cadence) is built-in. Unsupported extensions are preserved as key-value pairs. See [Extensions](05_Extensions.md). ## Processing pipeline diff --git a/docs/01_Usage/02_Creating_Files.md b/docs/01_Usage/02_Creating_Files.md index 8bdfc5d..0d2714f 100644 --- a/docs/01_Usage/02_Creating_Files.md +++ b/docs/01_Usage/02_Creating_Files.md @@ -121,6 +121,34 @@ $gpxFile->waypoints[] = $waypoint; $gpxFile->save('places.gpx', phpGPX::XML_FORMAT); ``` +## Adding extensions to points + +```php +use phpGPX\Models\Extensions; +use phpGPX\Models\Extensions\TrackPointExtension; + +$ext = new TrackPointExtension(); +$ext->hr = 145.0; +$ext->aTemp = 22.0; +$ext->cad = 85.0; + +$extensions = new Extensions(); +$extensions->set($ext); + +$point->extensions = $extensions; +``` + +Reading extensions back: + +```php +use phpGPX\Models\Extensions\TrackPointExtension; + +$ext = $point->extensions?->get(TrackPointExtension::class); +if ($ext !== null) { + echo "Heart rate: " . $ext->hr . " bpm\n"; +} +``` + ## Direct XML output to browser ```php diff --git a/docs/01_Usage/03_Statistics.md b/docs/01_Usage/03_Statistics.md index 4ceae83..18a055a 100644 --- a/docs/01_Usage/03_Statistics.md +++ b/docs/01_Usage/03_Statistics.md @@ -74,13 +74,13 @@ The engine walks the GPX structure **once** and dispatches each point to all reg ### Quick start with defaults ```php -$gpx = new phpGPX(engine: engine::default()); +$gpx = new phpGPX(engine: Engine::default()); ``` ### Customizing via the factory ```php -$gpx = new phpGPX(engine: engine::default( +$gpx = new phpGPX(engine: Engine::default( sortByTimestamp: true, applyElevationSmoothing: true, elevationSmoothingThreshold: 2, @@ -166,7 +166,7 @@ Aggregates Garmin TrackPointExtension sensor data (heart rate, cadence, temperat You can also use `engine` directly on a `GpxFile` you built programmatically: ```php -$gpxFile = engine::default()->process($gpxFile); +$gpxFile = Engine::default()->process($gpxFile); ``` ## Full example diff --git a/docs/01_Usage/04_Configuration.md b/docs/01_Usage/04_Configuration.md index 5d071c5..ed290fb 100644 --- a/docs/01_Usage/04_Configuration.md +++ b/docs/01_Usage/04_Configuration.md @@ -7,6 +7,16 @@ phpGPX is configured through two mechanisms: Each `phpGPX` instance carries its own configuration — there is no global state. +## phpGPX constructor + +```php +new phpGPX( + config: ?Config, // Output formatting (default: new Config()) + engine: ?Engine, // Stats analyzer engine (default: null — no stats) + extensionRegistry: ?ExtensionRegistry, // Extension namespace→parser mappings (default: ExtensionRegistry::default()) +); +``` + ## Config options ```php @@ -36,7 +46,7 @@ $gpx = new phpGPX(); // uses all defaults !!! note "Config is for output only" Processing behavior (stats calculation, smoothing, sorting) is controlled by `engine` and analyzer constructor arguments, not by Config. -## engine configuration +## Engine configuration ### Using the factory (recommended) @@ -108,12 +118,12 @@ $file = $gpx->load('track.gpx'); Since configuration is per-instance, you can use different settings for different files: ```php -$smooth = new phpGPX(engine: engine::default( +$smooth = new phpGPX(engine: Engine::default( applyElevationSmoothing: true, elevationSmoothingThreshold: 5, )); -$raw = new phpGPX(engine: engine::default()); +$raw = new phpGPX(engine: Engine::default()); $smoothFile = $smooth->load('track.gpx'); $rawFile = $raw->load('track.gpx'); @@ -123,4 +133,5 @@ $rawFile = $raw->load('track.gpx'); - Configuration is immutable after construction — `Config` properties are set once via constructor. - JSON output always uses ISO 8601 UTC for datetime values (GeoJSON convention). -- Stats are produced exclusively by `engine` and its analyzers — models are pure data containers. \ No newline at end of file +- Stats are produced exclusively by `Engine` and its analyzers — models are pure data containers. +- Extension registry is configured per-instance. See [Extensions](05_Extensions.md) for custom extension setup. \ No newline at end of file diff --git a/docs/01_Usage/05_Extensions.md b/docs/01_Usage/05_Extensions.md index af5b5fb..f944281 100644 --- a/docs/01_Usage/05_Extensions.md +++ b/docs/01_Usage/05_Extensions.md @@ -1,6 +1,46 @@ # Extensions -GPX 1.1 supports vendor-specific extensions. phpGPX parses known extensions into typed objects and preserves unknown ones. +GPX 1.1 supports vendor-specific extensions. phpGPX uses an **extension registry** to map XML namespace URIs to parser classes. Known extensions are parsed into typed objects; unknown ones are preserved as key-value pairs. + +## Extension Registry + +The `ExtensionRegistry` maps XML namespace URIs to parser classes. By default, Garmin TrackPointExtension (v1 + v2) is registered. + +```php +use phpGPX\phpGPX; +use phpGPX\Parsers\ExtensionRegistry; + +// Default registry (Garmin TrackPointExtension) +$gpx = new phpGPX(); + +// Or explicitly +$gpx = new phpGPX(extensionRegistry: ExtensionRegistry::default()); +``` + +### Registering custom extensions + +```php +use phpGPX\phpGPX; +use phpGPX\Parsers\ExtensionRegistry; + +// Via constructor +$gpx = new phpGPX( + extensionRegistry: ExtensionRegistry::default() + ->register('http://example.com/ext/v1', MyExtensionParser::class, 'myext'), +); + +// Or via method +$gpx = new phpGPX(); +$gpx->registerExtension('http://example.com/ext/v1', MyExtensionParser::class, 'myext'); +``` + +Multiple namespaces can map to the same parser (useful for v1/v2 aliasing): + +```php +$registry = (new ExtensionRegistry()) + ->register('http://example.com/ext/v1', MyExtensionParser::class, 'myext') + ->register('http://example.com/ext/v2', MyExtensionParser::class, 'myext'); +``` ## Garmin TrackPointExtension @@ -22,14 +62,17 @@ The most common extension. Provides sensor data per track point. ### Reading extensions ```php +use phpGPX\phpGPX; +use phpGPX\Models\Extensions\TrackPointExtension; + $gpx = new phpGPX(); $file = $gpx->load('garmin_track.gpx'); foreach ($file->tracks as $track) { foreach ($track->segments as $segment) { foreach ($segment->points as $point) { - if ($point->extensions && $point->extensions->trackPointExtension) { - $ext = $point->extensions->trackPointExtension; + $ext = $point->extensions?->get(TrackPointExtension::class); + if ($ext !== null) { echo "HR: " . $ext->hr . " bpm\n"; echo "Temp: " . $ext->aTemp . " C\n"; } @@ -49,7 +92,7 @@ $ext->hr = 145.0; $ext->aTemp = 22.0; $extensions = new Extensions(); -$extensions->trackPointExtension = $ext; +$extensions->set($ext); $point->extensions = $extensions; ``` @@ -58,12 +101,95 @@ The correct XML namespaces are handled automatically during serialization. ## Unsupported extensions -Extensions that phpGPX does not have a dedicated parser for are preserved as key-value pairs: +Extensions that have no registered parser are preserved as key-value pairs: ```php // Access unsupported extensions $unsupported = $point->extensions->unsupported; -// e.g. ['MxTimeZeroSymbol' => 10, 'color' => -16744448] +// e.g. ['MxTimeZeroSymbol' => '10', 'color' => '-16744448'] +``` + +Unsupported extensions are preserved during round-trip (load + save) and accessible through the `unsupported` array on the `Extensions` object. + +## Creating custom extensions + +To add support for a new GPX extension type, implement two interfaces: + +### 1. Extension model — `ExtensionInterface` + +```php +use phpGPX\Models\Extensions\AbstractExtension; +use phpGPX\Models\Extensions\ExtensionInterface; + +class MyExtension extends AbstractExtension implements ExtensionInterface +{ + public const NAMESPACE = 'http://example.com/ext/v1'; + public const XSD = 'http://example.com/ext/v1/schema.xsd'; + public const TAG = 'MyExtension'; + + public ?float $customValue = null; + + public static function getNamespace(): string { return self::NAMESPACE; } + public static function getSchemaLocation(): string { return self::XSD; } + public static function getTagName(): string { return self::TAG; } + + public function jsonSerialize(): mixed + { + return array_filter([ + 'customValue' => $this->customValue, + ], fn($v) => $v !== null); + } +} +``` + +### 2. Extension parser — `ExtensionParserInterface` + +```php +use phpGPX\Models\Extensions\ExtensionInterface; +use phpGPX\Parsers\Extensions\ExtensionParserInterface; + +class MyExtensionParser implements ExtensionParserInterface +{ + public static function parse(\SimpleXMLElement $node): ExtensionInterface + { + $ext = new MyExtension(); + $ext->customValue = isset($node->customValue) ? (float) $node->customValue : null; + return $ext; + } + + public static function toXML(ExtensionInterface $extension, \DOMDocument &$document, string $prefix): \DOMElement + { + $node = $document->createElement("$prefix:" . MyExtension::TAG); + + if ($extension->customValue !== null) { + $node->appendChild($document->createElement("$prefix:customValue", (string) $extension->customValue)); + } + + return $node; + } +} ``` -Unsupported extensions are preserved during round-trip (load + save) and accessible through the `unsupported` array on the `Extensions` object. \ No newline at end of file +### 3. Register it + +```php +$gpx = new phpGPX(); +$gpx->registerExtension(MyExtension::NAMESPACE, MyExtensionParser::class, 'myext'); + +$file = $gpx->load('file_with_custom_ext.gpx'); +``` + +The third argument is the XML namespace prefix used during serialization (defaults to `ext`). +During parsing, the prefix is extracted from the source XML automatically. + +## Extension statistics via Engine + +The `TrackPointExtensionAnalyzer` aggregates sensor data from `TrackPointExtension` into `Stats`: + +- `averageHeartRate`, `maxHeartRate` +- `averageCadence` +- `averageTemperature` + +These are computed per-segment and aggregated to track level (weighted by point count). See [Statistics](03_Statistics.md) for details. + +Custom extension analyzers can be built as `PointAnalyzerInterface` implementations and registered with the engine. See [Stats Architecture](../04_Development/04_Stats_Architecture.md). \ No newline at end of file diff --git a/docs/04_Development/04_Stats_Architecture.md b/docs/04_Development/04_Stats_Architecture.md new file mode 100644 index 0000000..8fc339a --- /dev/null +++ b/docs/04_Development/04_Stats_Architecture.md @@ -0,0 +1,235 @@ +# Stats Architecture: Single-Pass Analyzer Engine + +## Overview + +phpGPX 2.x uses a **single-pass analyzer engine** for computing GPS statistics. +A single `engine` walks the GPX structure once and dispatches each point to all +registered analyzers simultaneously. + +## Class Diagram + +```plantuml +@startuml +skinparam linetype ortho +skinparam nodesep 60 +skinparam ranksep 40 +skinparam backgroundColor transparent + +interface PointAnalyzerInterface { + +begin(): void + +visit(Point, ?Point): void + +end(Stats): void + +aggregateTrack(Track): void + +finalizeFile(GpxFile): void +} + +abstract class AbstractPointAnalyzer { + +aggregateTrack(Track): void + +finalizeFile(GpxFile): void +} + +class Engine { + -analyzers: PointAnalyzerInterface[] + -sortByTimestamp: bool + +addAnalyzer(PointAnalyzerInterface): self + +{static} default(...): self + +process(GpxFile): GpxFile + -sortPoints(GpxFile): void + -analyzePoints(Point[], Stats): void + -computeDerivedStats(Stats): void +} + +class DistanceAnalyzer { + -applySmoothing: bool + -smoothingThreshold: int +} + +class ElevationAnalyzer { + -ignoreZeroElevation: bool + -applySmoothing: bool + -smoothingThreshold: int + -spikesThreshold: ?int +} + +class AltitudeAnalyzer { + -ignoreZeroElevation: bool +} + +class TimestampAnalyzer +class BoundsAnalyzer +class MovementAnalyzer { + -speedThreshold: float +} +class TrackPointExtensionAnalyzer + +PointAnalyzerInterface <|.. AbstractPointAnalyzer +AbstractPointAnalyzer <|-- DistanceAnalyzer +AbstractPointAnalyzer <|-- ElevationAnalyzer +AbstractPointAnalyzer <|-- AltitudeAnalyzer +AbstractPointAnalyzer <|-- TimestampAnalyzer +AbstractPointAnalyzer <|-- BoundsAnalyzer +AbstractPointAnalyzer <|-- MovementAnalyzer +AbstractPointAnalyzer <|-- TrackPointExtensionAnalyzer + +Engine o-- "0..*" PointAnalyzerInterface : analyzers + +note right of Engine + Standalone class — no middleware + layer. Used directly by phpGPX + or called standalone via process(). + Sorting is built-in. +end note +@enduml +``` + +## Lifecycle Sequence + +```plantuml +@startuml +skinparam backgroundColor transparent + +participant "engine" as E +participant "Analyzer 1" as A1 +participant "Analyzer 2" as A2 +participant "Analyzer N" as AN + +opt sortByTimestamp + E -> E: sortPoints(gpxFile) +end + +group For each track + group For each segment + E -> A1: begin() + E -> A2: begin() + E -> AN: begin() + + group For each point (single pass) + E -> A1: visit(point, prev) + E -> A2: visit(point, prev) + E -> AN: visit(point, prev) + end + + E -> A1: end(segmentStats) + E -> A2: end(segmentStats) + E -> AN: end(segmentStats) + + E -> E: computeDerivedStats(segmentStats) + end + + E -> A1: aggregateTrack(track) + E -> A2: aggregateTrack(track) + E -> AN: aggregateTrack(track) + E -> E: computeDerivedStats(trackStats) +end + +group For each route + E -> A1: begin() + E -> A2: begin() + E -> AN: begin() + note right: Same visit loop as segments + E -> A1: end(routeStats) + E -> A2: end(routeStats) + E -> AN: end(routeStats) + E -> E: computeDerivedStats(routeStats) +end + +E -> A1: finalizeFile(gpxFile) +E -> A2: finalizeFile(gpxFile) +E -> AN: finalizeFile(gpxFile) + +@enduml +``` + +## How It Fits in phpGPX + +```plantuml +@startuml +skinparam backgroundColor transparent + +rectangle "phpGPX::parse()" as parse +rectangle "Engine::process()\n(sort + single-pass analysis)" as engine +rectangle "Return GpxFile" as ret + +parse -right-> engine : GpxFile +engine -right-> ret : GpxFile + +note bottom of engine + Optional. Sorting and all analysis + happen in one step. No middleware + pipeline. +end note +@enduml +``` + +## Built-in Analyzers + +| Analyzer | Computes | Config | +|-------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------| +| `DistanceAnalyzer` | Raw distance, real distance, per-point difference/distance | `applySmoothing`, `smoothingThreshold` | +| `ElevationAnalyzer` | Cumulative elevation gain/loss | `ignoreZeroElevation`, `applySmoothing`, `smoothingThreshold`, `spikesThreshold` | +| `AltitudeAnalyzer` | Min/max altitude with coordinates | `ignoreZeroElevation` | +| `TimestampAnalyzer` | Start/end timestamps with coordinates | — | +| `BoundsAnalyzer` | Lat/lon bounding box (segment, track, file) | — | +| `MovementAnalyzer` | Moving duration, moving average speed | `speedThreshold` | +| `TrackPointExtensionAnalyzer` | HR, cadence, temperature averages/max | — | + +**Derived stats** (computed by the engine after all analyzers finish): +- Duration = finishedAt - startedAt +- Average speed = distance / duration +- Average pace = duration / (distance / 1000) +- Moving average speed = distance / movingDuration + +## Extending with Custom Analyzers + +To add a new statistic, extend `AbstractPointAnalyzer`: + +```php +use phpGPX\Analysis\AbstractPointAnalyzer; +use phpGPX\Models\Point; +use phpGPX\Models\Stats; +use phpGPX\Models\Track; + +class MaxSpeedAnalyzer extends AbstractPointAnalyzer +{ + private float $maxSpeed = 0; + + public function begin(): void + { + $this->maxSpeed = 0; + } + + public function visit(Point $current, ?Point $previous): void + { + if ($previous === null || $previous->time === null || $current->time === null) { + return; + } + + $timeDelta = abs($current->time->getTimestamp() - $previous->time->getTimestamp()); + if ($timeDelta === 0) return; + + $distance = \phpGPX\Helpers\GeoHelper::getRawDistance($previous, $current); + $speed = $distance / $timeDelta; + + if ($speed > $this->maxSpeed) { + $this->maxSpeed = $speed; + } + } + + public function end(Stats $stats): void + { + // Write to stats (you may need to add a custom field or use extensions) + } + + public function aggregateTrack(Track $track): void + { + // Find max across segments + } +} +``` + +Then register it: + +```php +$engine = Engine::default(); +$engine->addAnalyzer(new MaxSpeedAnalyzer()); +``` diff --git a/docs/index.md b/docs/index.md index 5fa0c2b..b9f119c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,17 +9,17 @@ A PHP library for reading, creating, and manipulating [GPX files](https://en.wik ## Features - Full support of [GPX 1.1 specification](http://www.topografix.com/GPX/1/1/) -- Statistics calculation (distance, elevation, speed, pace, duration) -- Extension support (Garmin TrackPointExtension) +- Single-pass stats engine with pluggable analyzers +- Extension registry — built-in Garmin TrackPointExtension, custom extensions via `ExtensionInterface` - GeoJSON output (RFC 7946) and GPX XML output -- Instance-based API with injectable configuration ## Quick Example ```php use phpGPX\phpGPX; +use phpGPX\Analysis\Engine; -$gpx = new phpGPX(); +$gpx = new phpGPX(engine: Engine::default()); $file = $gpx->load('track.gpx'); foreach ($file->tracks as $track) { diff --git a/examples/CreateFileFromScratch.php b/examples/CreateFileFromScratch.php index b60f0cd..a878bbe 100644 --- a/examples/CreateFileFromScratch.php +++ b/examples/CreateFileFromScratch.php @@ -94,7 +94,7 @@ $point->extensions = new Extensions(); $trackPointExtension = new TrackPointExtension(); $trackPointExtension->aTemp = $sample_point['aTemp']; - $point->extensions->trackPointExtension = $trackPointExtension; + $point->extensions->set($trackPointExtension); $segment->points[] = $point; } diff --git a/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php index 74cb437..cfeceb9 100644 --- a/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php +++ b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php @@ -2,6 +2,7 @@ namespace phpGPX\Analysis; +use phpGPX\Models\Extensions\TrackPointExtension; use phpGPX\Models\Point; use phpGPX\Models\Stats; use phpGPX\Models\Track; @@ -62,7 +63,7 @@ public function begin(): void public function visit(Point $current, ?Point $previous): void { - $ext = $current->extensions?->trackPointExtension; + $ext = $current->extensions?->get(TrackPointExtension::class); if ($ext === null) { return; diff --git a/src/phpGPX/Models/Extensions.php b/src/phpGPX/Models/Extensions.php index 1749929..d2df082 100644 --- a/src/phpGPX/Models/Extensions.php +++ b/src/phpGPX/Models/Extensions.php @@ -1,45 +1,100 @@ - */ namespace phpGPX\Models; -use phpGPX\Models\Extensions\TrackPointExtension; +use phpGPX\Models\Extensions\ExtensionInterface; /** - * Class Extensions - * TODO: http://www.garmin.com/xmlschemas/GpxExtensions/v3 - * @package phpGPX\Models + * Container for GPX extensions on a Point, Track, Route, or GpxFile. + * + * Holds registered (typed) extensions keyed by class name, plus an array + * of unsupported extension data preserved as raw key-value strings for + * round-trip fidelity. + * + * ## Accessing extensions + * + * ```php + * use phpGPX\Models\Extensions\TrackPointExtension; + * + * $ext = $point->extensions?->get(TrackPointExtension::class); + * if ($ext !== null) { + * echo $ext->hr; // heart rate + * } + * ``` */ class Extensions implements \JsonSerializable { + /** @var array, ExtensionInterface> */ + private array $items = []; + /** - * GPX Garmin TrackPointExtension v1 - * @see 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1' - * @var TrackPointExtension|null + * Unsupported extensions preserved as key-value pairs. + * Keys are prefixed element names (e.g., "ns:ElementName"), values are string content. + * @var array */ - public ?TrackPointExtension $trackPointExtension; + public array $unsupported = []; /** - * @var array + * Store a typed extension. */ - public array $unsupported = []; + public function set(ExtensionInterface $extension): void + { + $this->items[get_class($extension)] = $extension; + } /** - * Extensions constructor. + * Retrieve a typed extension by class name. + * + * @template T of ExtensionInterface + * @param class-string $class + * @return T|null */ - public function __construct() + public function get(string $class): ?ExtensionInterface { - $this->trackPointExtension = null; + return $this->items[$class] ?? null; } - public function jsonSerialize(): array + /** + * Check if a typed extension is present. + * + * @param class-string $class + */ + public function has(string $class): bool { - return array_filter([ - 'trackpoint' => $this->trackPointExtension, - 'unsupported' => !empty($this->unsupported) ? $this->unsupported : null, - ], fn($v) => $v !== null); + return isset($this->items[$class]); + } + + /** + * Get all typed extensions. + * + * @return array, ExtensionInterface> + */ + public function all(): array + { + return $this->items; + } + + /** + * Check if this container has any data (typed or unsupported). + */ + public function isEmpty(): bool + { + return empty($this->items) && empty($this->unsupported); + } + + public function jsonSerialize(): mixed + { + $result = []; + + foreach ($this->items as $ext) { + $key = lcfirst($ext::getTagName()); + $result[$key] = $ext; + } + + if (!empty($this->unsupported)) { + $result['unsupported'] = $this->unsupported; + } + + return !empty($result) ? $result : new \stdClass(); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Extensions/AbstractExtension.php b/src/phpGPX/Models/Extensions/AbstractExtension.php index b46ab2c..843ab14 100644 --- a/src/phpGPX/Models/Extensions/AbstractExtension.php +++ b/src/phpGPX/Models/Extensions/AbstractExtension.php @@ -8,27 +8,4 @@ abstract class AbstractExtension implements \JsonSerializable { - - /** - * XML namespace of extension - * @var string - */ - public string $namespace; - - /** - * Node name extension. - * @var string - */ - public string $extensionName; - - /** - * AbstractExtension constructor. - * @param string $namespace - * @param string $extensionName - */ - public function __construct(string $namespace, string $extensionName) - { - $this->namespace = $namespace; - $this->extensionName = $extensionName; - } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Extensions/ExtensionInterface.php b/src/phpGPX/Models/Extensions/ExtensionInterface.php new file mode 100644 index 0000000..a2b3c46 --- /dev/null +++ b/src/phpGPX/Models/Extensions/ExtensionInterface.php @@ -0,0 +1,57 @@ + $this->value ?? null], fn($v) => $v !== null); } + * } + * ``` + */ +interface ExtensionInterface extends \JsonSerializable +{ + /** + * XML namespace URI for this extension. + * + * Example: `http://www.garmin.com/xmlschemas/TrackPointExtension/v2` + */ + public static function getNamespace(): string; + + /** + * XSD schema location URL. + * + * Example: `http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd` + */ + public static function getSchemaLocation(): string; + + /** + * Root XML element name within the `` block. + * + * Example: `TrackPointExtension` + */ + public static function getTagName(): string; +} \ No newline at end of file diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php index 02da3b3..d010545 100644 --- a/src/phpGPX/Models/Extensions/TrackPointExtension.php +++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php @@ -7,89 +7,57 @@ namespace phpGPX\Models\Extensions; /** - * Class TrackPointExtension - * Extension version: v2 - * Based on namespace: http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd - * @package phpGPX\Models\Extensions + * Garmin TrackPointExtension model (v2). + * + * Provides sensor data per track point: heart rate, cadence, temperature, etc. + * + * @see https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd */ -class TrackPointExtension extends AbstractExtension +class TrackPointExtension extends AbstractExtension implements ExtensionInterface { - const EXTENSION_V1_NAMESPACE = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1'; - const EXTENSION_V1_NAMESPACE_XSD = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd'; + const NAMESPACE_URI = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2'; + const SCHEMA_LOCATION = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd'; + const TAG_NAME = 'TrackPointExtension'; - const EXTENSION_NAMESPACE = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2'; - const EXTENSION_NAMESPACE_XSD = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd'; + public static function getNamespace(): string { return self::NAMESPACE_URI; } + public static function getSchemaLocation(): string { return self::SCHEMA_LOCATION; } + public static function getTagName(): string { return self::TAG_NAME; } - const EXTENSION_NAME = 'TrackPointExtension'; - const EXTENSION_NAMESPACE_PREFIX = 'gpxtpx'; + /** Air temperature in degrees Celsius. */ + public ?float $aTemp = null; - /** - * Average temperature value measured in degrees Celsius. - * @var float|null - */ - public ?float $aTemp; + /** Water temperature in degrees Celsius. */ + public ?float $wTemp = null; - /** - * @var float|null - */ - public ?float $wTemp; + /** Depth in meters. */ + public ?float $depth = null; - /** - * Depth in meters. - * @var float|null - */ - public ?float $depth; + /** Heart rate in beats per minute. */ + public ?float $hr = null; - /** - * Heart rate in beats per minute. - * @since v1.0RC3 - * @var float|null - */ - public ?float $hr; + /** Cadence in revolutions per minute. */ + public ?float $cad = null; - /** - * Cadence in revolutions per minute. - * @var float|null - */ - public ?float $cad; + /** Speed in meters per second. */ + public ?float $speed = null; - /** - * Speed in meters per second. - * @var float|null - */ - public ?float $speed; + /** Course in degrees from true north. */ + public ?int $course = null; - /** - * Course. This type contains an angle measured in degrees in a clockwise direction from the true north line. - * @var int|null - */ - public ?int $course; - - /** - * Bearing. This type contains an angle measured in degrees in a clockwise direction from the true north line. - * @var int|null - */ - public ?int $bearing; - - /** - * TrackPointExtension constructor. - */ - public function __construct() - { - parent::__construct(self::EXTENSION_NAMESPACE, self::EXTENSION_NAME); - } + /** Bearing in degrees from true north. */ + public ?int $bearing = null; public function jsonSerialize(): array { return array_filter([ - 'aTemp' => $this->aTemp ?? null, - 'wTemp' => $this->wTemp ?? null, - 'depth' => $this->depth ?? null, - 'hr' => $this->hr ?? null, - 'cad' => $this->cad ?? null, - 'speed' => $this->speed ?? null, - 'course' => $this->course ?? null, - 'bearing' => $this->bearing ?? null, + 'aTemp' => $this->aTemp, + 'wTemp' => $this->wTemp, + 'depth' => $this->depth, + 'hr' => $this->hr, + 'cad' => $this->cad, + 'speed' => $this->speed, + 'course' => $this->course, + 'bearing' => $this->bearing, ], fn($v) => $v !== null); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php index 1e81ada..19804c6 100644 --- a/src/phpGPX/Models/GpxFile.php +++ b/src/phpGPX/Models/GpxFile.php @@ -8,6 +8,7 @@ use phpGPX\Config; use phpGPX\Parsers\ExtensionParser; +use phpGPX\Parsers\ExtensionRegistry; use phpGPX\Parsers\MetadataParser; use phpGPX\Parsers\PointParser; use phpGPX\Parsers\RouteParser; @@ -36,6 +37,8 @@ class GpxFile implements \JsonSerializable public ?string $creator = null; + public ?string $version = null; + public function __construct( public readonly Config $config = new Config(), ) {} @@ -88,10 +91,11 @@ public function toXML(): \DOMDocument $document = new \DOMDocument("1.0", 'UTF-8'); $gpx = $document->createElementNS("http://www.topografix.com/GPX/1/1", "gpx"); - $gpx->setAttribute("version", "1.1"); + $gpx->setAttribute("version", $this->version ?? "1.1"); $gpx->setAttribute("creator", $this->creator ? $this->creator : phpGPX::getSignature()); ExtensionParser::$usedNamespaces = []; + ExtensionParser::$registry ??= ExtensionRegistry::default(); if (!empty($this->metadata)) { $gpx->appendChild(MetadataParser::toXML($this->metadata, $document)); @@ -109,7 +113,7 @@ public function toXML(): \DOMDocument $gpx->appendChild(TrackParser::toXML($track, $document)); } - if (!empty($this->extensions)) { + if ($this->extensions !== null && !$this->extensions->isEmpty()) { $gpx->appendChild(ExtensionParser::toXML($this->extensions, $document)); } diff --git a/src/phpGPX/Parsers/AbstractParser.php b/src/phpGPX/Parsers/AbstractParser.php index e6351af..b8af5b4 100644 --- a/src/phpGPX/Parsers/AbstractParser.php +++ b/src/phpGPX/Parsers/AbstractParser.php @@ -138,6 +138,11 @@ protected static function serializeDelegated(mixed $value, array $attribute, \DO return; } + // Skip empty Extensions containers — avoid emitting + if ($value instanceof \phpGPX\Models\Extensions && $value->isEmpty()) { + return; + } + $parserClass = $attribute['parser']; if ($attribute['type'] === 'array') { diff --git a/src/phpGPX/Parsers/ExtensionParser.php b/src/phpGPX/Parsers/ExtensionParser.php index 07f0814..cce3ab4 100644 --- a/src/phpGPX/Parsers/ExtensionParser.php +++ b/src/phpGPX/Parsers/ExtensionParser.php @@ -1,67 +1,84 @@ - */ namespace phpGPX\Parsers; use phpGPX\Models\Extensions; -use phpGPX\Models\Extensions\TrackPointExtension; -use phpGPX\Parsers\Extensions\TrackPointExtensionParser; /** - * Class ExtensionParser - * @package phpGPX\Parsers + * Parses and serializes `` blocks using the extension registry. + * + * During parsing, each namespace encountered in an `` element is + * looked up in the {@see ExtensionRegistry}. If a parser is registered for that + * namespace, the child elements are delegated to it. Otherwise, they are stored + * as unsupported key-value pairs. + * + * The registry is set by `phpGPX` before parsing and serialization via the + * static `$registry` property. */ abstract class ExtensionParser { public static string $tagName = 'extensions'; + /** @var array */ public static array $usedNamespaces = []; /** - * @param \SimpleXMLElement $nodes - * @return Extensions + * The active extension registry. Set by phpGPX before parse/serialize operations. + */ + public static ?ExtensionRegistry $registry = null; + + /** + * Parse an `` XML element into an Extensions container. */ public static function parse(\SimpleXMLElement $nodes): Extensions { $extensions = new Extensions(); - $nodeNamespaces = $nodes->getNamespaces(true); + $registry = self::$registry; - foreach ($nodeNamespaces as $key => $namespace) { - switch ($namespace) { - case TrackPointExtension::EXTENSION_NAMESPACE: - case TrackPointExtension::EXTENSION_V1_NAMESPACE: - $node = $nodes->children($namespace)->{TrackPointExtension::EXTENSION_NAME}; - if (!empty($node)) { - $extensions->trackPointExtension = TrackPointExtensionParser::parse($node); - } - break; - default: - foreach ($nodes->children($namespace) as $child_key => $value) { - $extensions->unsupported[$key ? "$key:$child_key" : "$child_key"] = (string) $value; - } + foreach ($nodeNamespaces as $prefix => $namespace) { + $parserClass = $registry?->getParserClass($namespace); + + if ($parserClass !== null) { + foreach ($nodes->children($namespace) as $child) { + $ext = $parserClass::parse($child); + $extensions->set($ext); + } + } else { + foreach ($nodes->children($namespace) as $childKey => $value) { + $extensions->unsupported[$prefix ? "$prefix:$childKey" : "$childKey"] = (string) $value; + } } } return $extensions; } - /** - * @param Extensions $extensions - * @param \DOMDocument $document - * @return \DOMElement|null + * Serialize an Extensions container to a DOM element. */ public static function toXML(Extensions $extensions, \DOMDocument &$document): \DOMElement { - $node = $document->createElement(self::$tagName); + $node = $document->createElement(self::$tagName); + $registry = self::$registry; + + foreach ($extensions->all() as $ext) { + $namespace = $ext::getNamespace(); + $parserClass = $registry?->getParserClass($namespace); - if (null !== $extensions->trackPointExtension) { - $child = TrackPointExtensionParser::toXML($extensions->trackPointExtension, $document); - $node->appendChild($child); + if ($parserClass !== null) { + $prefix = $registry->getPrefix($namespace) ?? 'ext'; + $child = $parserClass::toXML($ext, $document, $prefix); + $node->appendChild($child); + + // Register namespace for schema location output in GpxFile::toXML() + self::$usedNamespaces[$ext::getTagName()] = [ + 'namespace' => $namespace, + 'xsd' => $ext::getSchemaLocation(), + 'name' => $ext::getTagName(), + 'prefix' => $prefix, + ]; + } } if (!empty($extensions->unsupported)) { @@ -73,4 +90,4 @@ public static function toXML(Extensions $extensions, \DOMDocument &$document): \ return $node; } -} +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/ExtensionRegistry.php b/src/phpGPX/Parsers/ExtensionRegistry.php new file mode 100644 index 0000000..71edcbb --- /dev/null +++ b/src/phpGPX/Parsers/ExtensionRegistry.php @@ -0,0 +1,98 @@ +` blocks. During serialization, the registry + * provides the namespace prefix for XML element names. + * + * ## Default registry + * + * The `default()` factory registers Garmin TrackPointExtension (both v1 and v2 + * namespaces) with the `gpxtpx` prefix. + * + * ## Custom extensions + * + * ```php + * use phpGPX\Parsers\ExtensionRegistry; + * + * $registry = ExtensionRegistry::default() + * ->register('http://example.com/ext/v1', MyExtensionParser::class, 'myext'); + * + * $gpx = new phpGPX(extensionRegistry: $registry); + * ``` + * + * Multiple namespaces can map to the same parser (e.g., v1 and v2 of the same extension). + */ +class ExtensionRegistry +{ + /** @var array, prefix: string}> */ + private array $entries = []; + + /** + * Register a parser for a namespace URI. + * + * @param string $namespace The XML namespace URI + * @param string $parserClass Fully qualified class name implementing ExtensionParserInterface + * @param string $prefix XML namespace prefix for serialization (e.g., 'gpxtpx') + * @return $this Fluent interface + */ + public function register(string $namespace, string $parserClass, string $prefix = 'ext'): self + { + $this->entries[$namespace] = ['parserClass' => $parserClass, 'prefix' => $prefix]; + return $this; + } + + /** + * Get the parser class for a namespace, or null if not registered. + */ + public function getParserClass(string $namespace): ?string + { + return $this->entries[$namespace]['parserClass'] ?? null; + } + + /** + * Get the XML prefix for a namespace, or null if not registered. + */ + public function getPrefix(string $namespace): ?string + { + return $this->entries[$namespace]['prefix'] ?? null; + } + + /** + * Check if a namespace is registered. + */ + public function has(string $namespace): bool + { + return isset($this->entries[$namespace]); + } + + /** + * Get all registered namespace → parser mappings. + * + * @return array + */ + public function all(): array + { + return $this->entries; + } + + /** + * Create a registry with the standard built-in extensions. + * + * Registers Garmin TrackPointExtension for both v1 and v2 namespaces + * with the `gpxtpx` prefix. + */ + public static function default(): self + { + return (new self()) + ->register('http://www.garmin.com/xmlschemas/TrackPointExtension/v2', TrackPointExtensionParser::class, 'gpxtpx') + ->register('http://www.garmin.com/xmlschemas/TrackPointExtension/v1', TrackPointExtensionParser::class, 'gpxtpx'); + } +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php b/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php new file mode 100644 index 0000000..2414dfc --- /dev/null +++ b/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php @@ -0,0 +1,47 @@ +`) + * @return ExtensionInterface The populated extension model + */ + public static function parse(\SimpleXMLElement $node): ExtensionInterface; + + /** + * Serialize an extension model to a DOM element. + * + * The prefix is provided by the registry — extension parsers should use it + * for element names (e.g., `$prefix:TrackPointExtension`) rather than + * hardcoding a prefix. + * + * @param ExtensionInterface $extension The extension model to serialize + * @param \DOMDocument $document The parent DOM document + * @param string $prefix XML namespace prefix from the registry + * @return \DOMElement The serialized XML element + */ + public static function toXML(ExtensionInterface $extension, \DOMDocument &$document, string $prefix): \DOMElement; +} \ No newline at end of file diff --git a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php index 0e789af..67b579a 100644 --- a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php +++ b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php @@ -6,11 +6,11 @@ namespace phpGPX\Parsers\Extensions; +use phpGPX\Models\Extensions\ExtensionInterface; use phpGPX\Models\Extensions\TrackPointExtension; use phpGPX\Parsers\AbstractParser; -use phpGPX\Parsers\ExtensionParser; -class TrackPointExtensionParser extends AbstractParser +class TrackPointExtensionParser extends AbstractParser implements ExtensionParserInterface { protected static function getAttributeMapper(): array { @@ -50,11 +50,7 @@ protected static function getAttributeMapper(): array ]; } - /** - * @param \SimpleXMLElement $node - * @return TrackPointExtension - */ - public static function parse(\SimpleXMLElement $node): TrackPointExtension + public static function parse(\SimpleXMLElement $node): ExtensionInterface { $extension = new TrackPointExtension(); @@ -63,26 +59,14 @@ public static function parse(\SimpleXMLElement $node): TrackPointExtension return $extension; } - /** - * @param TrackPointExtension $extension - * @param \DOMDocument $document - * @return \DOMElement - */ - public static function toXML(TrackPointExtension $extension, \DOMDocument &$document): \DOMElement + public static function toXML(ExtensionInterface $extension, \DOMDocument &$document, string $prefix = 'gpxtpx'): \DOMElement { - $node = $document->createElement("gpxtpx:TrackPointExtension"); - - ExtensionParser::$usedNamespaces[TrackPointExtension::EXTENSION_NAME] = [ - 'namespace' => TrackPointExtension::EXTENSION_NAMESPACE, - 'xsd' => TrackPointExtension::EXTENSION_NAMESPACE_XSD, - 'name' => TrackPointExtension::EXTENSION_NAME, - 'prefix' => TrackPointExtension::EXTENSION_NAMESPACE_PREFIX - ]; + $node = $document->createElement(sprintf("%s:%s", $prefix, $extension::getTagName())); foreach (self::getAttributeMapper() as $key => $attribute) { if (isset($extension->{$attribute['name']})) { $child = $document->createElement( - sprintf("%s:%s", TrackPointExtension::EXTENSION_NAMESPACE_PREFIX, $key), + sprintf("%s:%s", $prefix, $key), $extension->{$attribute['name']} ); $node->appendChild($child); diff --git a/src/phpGPX/Parsers/SegmentParser.php b/src/phpGPX/Parsers/SegmentParser.php index 18bb5e8..2067772 100644 --- a/src/phpGPX/Parsers/SegmentParser.php +++ b/src/phpGPX/Parsers/SegmentParser.php @@ -57,7 +57,7 @@ public static function toXML(Segment $segment, \DOMDocument &$document): \DOMEle $node->appendChild(PointParser::toXML($point, $document)); } - if (!empty($segment->extensions)) { + if ($segment->extensions !== null && !$segment->extensions->isEmpty()) { $node->appendChild(ExtensionParser::toXML($segment->extensions, $document)); } diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index 9b1f232..1a523fb 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -8,6 +8,8 @@ use phpGPX\Analysis\Engine; use phpGPX\Models\GpxFile; +use phpGPX\Parsers\ExtensionParser; +use phpGPX\Parsers\ExtensionRegistry; use phpGPX\Parsers\MetadataParser; use phpGPX\Parsers\RouteParser; use phpGPX\Parsers\TrackParser; @@ -24,16 +26,22 @@ class phpGPX const GEOJSON_FORMAT = 'geojson'; const PACKAGE_NAME = 'phpGPX'; - const VERSION = '2.0.0-alpha.2'; + const VERSION = '2.0.0-alpha.3'; public readonly Config $config; private ?Engine $engine = null; - public function __construct(?Config $config = null, ?Engine $engine = null) - { + private ExtensionRegistry $extensionRegistry; + + public function __construct( + ?Config $config = null, + ?Engine $engine = null, + ?ExtensionRegistry $extensionRegistry = null, + ) { $this->config = $config ?? new Config(); $this->engine = $engine; + $this->extensionRegistry = $extensionRegistry ?? ExtensionRegistry::default(); } /** @@ -47,6 +55,20 @@ public function setEngine(Engine $engine): self return $this; } + /** + * Register an extension parser for a namespace URI. + * + * @param string $namespace The XML namespace URI + * @param string $parserClass Fully qualified class implementing ExtensionParserInterface + * @param string $prefix XML namespace prefix for serialization (e.g., 'gpxtpx') + * @return $this Fluent interface + */ + public function registerExtension(string $namespace, string $parserClass, string $prefix = 'ext'): self + { + $this->extensionRegistry->register($namespace, $parserClass, $prefix); + return $this; + } + /** * Load GPX file from path. */ @@ -62,9 +84,13 @@ public function parse(string $xml): GpxFile { $xmlElement = simplexml_load_string($xml); + // Configure extension parser with our registry + ExtensionParser::$registry = $this->extensionRegistry; + $gpx = new GpxFile($this->config); $gpx->creator = isset($xmlElement['creator']) ? (string)$xmlElement['creator'] : null; + $gpx->version = isset($xmlElement['version']) ? (string)$xmlElement['version'] : null; $gpx->metadata = isset($xmlElement->metadata) ? MetadataParser::parse($xmlElement->metadata) : null; $gpx->waypoints = isset($xmlElement->wpt) ? WaypointParser::parse($xmlElement->wpt) : []; $gpx->tracks = isset($xmlElement->trk) ? TrackParser::parse($xmlElement->trk) : []; diff --git a/tests/Fixtures/Parsers/Extension/extension.json b/tests/Fixtures/Parsers/Extension/extension.json index 4c3f1b7..1f809ce 100644 --- a/tests/Fixtures/Parsers/Extension/extension.json +++ b/tests/Fixtures/Parsers/Extension/extension.json @@ -1,5 +1,5 @@ { - "trackpoint": { + "trackPointExtension": { "aTemp": 14, "hr": 152 } diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php index 43fc49b..12a5c54 100644 --- a/tests/Integration/GpxFileLoadTest.php +++ b/tests/Integration/GpxFileLoadTest.php @@ -132,8 +132,8 @@ public function testLoadMinimalGpx(): void // Check TrackPointExtension (heart rate) $firstPoint = $gpxFile->tracks[0]->segments[0]->points[0]; $this->assertNotNull($firstPoint->extensions); - $this->assertNotNull($firstPoint->extensions->trackPointExtension); - $this->assertEqualsWithDelta(126, $firstPoint->extensions->trackPointExtension->hr, 0.1); + $this->assertNotNull($firstPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)); + $this->assertEqualsWithDelta(126, $firstPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr, 0.1); } public function testLoadCreatorAttribute(): void @@ -142,6 +142,19 @@ public function testLoadCreatorAttribute(): void $this->assertEquals('RouteConverter', $gpxFile->creator); } + public function testLoadVersionAttribute(): void + { + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); + $this->assertEquals('1.1', $gpxFile->version); + } + + public function testVersionPreservedInXmlOutput(): void + { + $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx'); + $xml = $gpxFile->toXML()->saveXML(); + $this->assertStringContainsString('version="1.1"', $xml); + } + public function testParseFromString(): void { $xml = file_get_contents(self::FIXTURES_DIR . '/route.gpx'); diff --git a/tests/Integration/XmlRoundTripTest.php b/tests/Integration/XmlRoundTripTest.php index 4bab901..9c9398f 100644 --- a/tests/Integration/XmlRoundTripTest.php +++ b/tests/Integration/XmlRoundTripTest.php @@ -125,10 +125,10 @@ public function testRoundTripMinimalWithExtensions(): void $reloadedPoint = $reloaded->tracks[0]->segments[0]->points[0]; $this->assertNotNull($reloadedPoint->extensions); - $this->assertNotNull($reloadedPoint->extensions->trackPointExtension); + $this->assertNotNull($reloadedPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)); $this->assertEqualsWithDelta( - $origPoint->extensions->trackPointExtension->hr, - $reloadedPoint->extensions->trackPointExtension->hr, + $origPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr, + $reloadedPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr, 0.1 ); } diff --git a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php index 49716bf..394451d 100644 --- a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php +++ b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php @@ -40,7 +40,7 @@ private function makePointWithExtension( $ext->aTemp = $aTemp; $extensions = new Extensions(); - $extensions->trackPointExtension = $ext; + $extensions->set($ext); $p->extensions = $extensions; } diff --git a/tests/Unit/Parsers/ExtensionParserTest.php b/tests/Unit/Parsers/ExtensionParserTest.php index fabf699..c41c9dc 100644 --- a/tests/Unit/Parsers/ExtensionParserTest.php +++ b/tests/Unit/Parsers/ExtensionParserTest.php @@ -5,6 +5,7 @@ use phpGPX\Models\Extensions; use phpGPX\Models\Extensions\TrackPointExtension; use phpGPX\Parsers\ExtensionParser; +use phpGPX\Parsers\ExtensionRegistry; use PHPUnit\Framework\TestCase; class ExtensionParserTest extends TestCase @@ -21,9 +22,12 @@ protected function setUp(): void $trackpoint->hr = 152.0; $this->extensions = new Extensions(); - $this->extensions->trackPointExtension = $trackpoint; + $this->extensions->set($trackpoint); $this->file = simplexml_load_file(self::FIXTURES_DIR . '/extension.xml'); + + // Configure the registry for parsing + ExtensionParser::$registry = ExtensionRegistry::default(); } public function testParse(): void @@ -31,9 +35,11 @@ public function testParse(): void $extensions = ExtensionParser::parse($this->file->extensions); $this->assertEquals($this->extensions->unsupported, $extensions->unsupported); - $this->assertEquals( - $this->extensions->trackPointExtension->jsonSerialize(), $extensions->trackPointExtension->jsonSerialize() - ); + + $parsed = $extensions->get(TrackPointExtension::class); + $expected = $this->extensions->get(TrackPointExtension::class); + $this->assertNotNull($parsed); + $this->assertEquals($expected->jsonSerialize(), $parsed->jsonSerialize()); $this->assertJsonStringEqualsJsonString( json_encode($this->extensions), json_encode($extensions) diff --git a/tests/Unit/Parsers/ExtensionRegistryTest.php b/tests/Unit/Parsers/ExtensionRegistryTest.php new file mode 100644 index 0000000..762d03f --- /dev/null +++ b/tests/Unit/Parsers/ExtensionRegistryTest.php @@ -0,0 +1,99 @@ +assertTrue($registry->has(self::GARMIN_TPE_V2)); + $this->assertTrue($registry->has(self::GARMIN_TPE_V1)); + $this->assertSame( + TrackPointExtensionParser::class, + $registry->getParserClass(self::GARMIN_TPE_V2) + ); + } + + public function testRegisterReturnsFluentInterface(): void + { + $registry = new ExtensionRegistry(); + $result = $registry->register('http://example.com/ext', TrackPointExtensionParser::class); + $this->assertSame($registry, $result); + } + + public function testGetParserClassReturnsNullForUnknown(): void + { + $registry = new ExtensionRegistry(); + $this->assertNull($registry->getParserClass('http://unknown.com/ext')); + } + + public function testHasReturnsFalseForUnknown(): void + { + $registry = new ExtensionRegistry(); + $this->assertFalse($registry->has('http://unknown.com/ext')); + } + + public function testCustomRegistration(): void + { + $registry = ExtensionRegistry::default() + ->register('http://example.com/custom/v1', TrackPointExtensionParser::class); + + $this->assertTrue($registry->has('http://example.com/custom/v1')); + $this->assertTrue($registry->has(self::GARMIN_TPE_V2)); + } + + public function testAllReturnsRegisteredMappings(): void + { + $registry = ExtensionRegistry::default(); + $all = $registry->all(); + + $this->assertArrayHasKey(self::GARMIN_TPE_V2, $all); + $this->assertArrayHasKey(self::GARMIN_TPE_V1, $all); + $this->assertCount(2, $all); + } + + public function testEmptyRegistryHasNoMappings(): void + { + $registry = new ExtensionRegistry(); + $this->assertEmpty($registry->all()); + } + + public function testDefaultPrefixIsGpxtpx(): void + { + $registry = ExtensionRegistry::default(); + + $this->assertSame('gpxtpx', $registry->getPrefix(self::GARMIN_TPE_V2)); + $this->assertSame('gpxtpx', $registry->getPrefix(self::GARMIN_TPE_V1)); + } + + public function testCustomPrefix(): void + { + $registry = (new ExtensionRegistry()) + ->register('http://example.com/ext', TrackPointExtensionParser::class, 'myprefix'); + + $this->assertSame('myprefix', $registry->getPrefix('http://example.com/ext')); + } + + public function testGetPrefixReturnsNullForUnknown(): void + { + $registry = new ExtensionRegistry(); + $this->assertNull($registry->getPrefix('http://unknown.com/ext')); + } + + public function testDefaultPrefixIsExt(): void + { + $registry = (new ExtensionRegistry()) + ->register('http://example.com/ext', TrackPointExtensionParser::class); + + $this->assertSame('ext', $registry->getPrefix('http://example.com/ext')); + } +} \ No newline at end of file From 3fb233d65911d32b8afaf8987602951969735969 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Sun, 8 Mar 2026 23:57:16 +0100 Subject: [PATCH 21/31] =?UTF-8?q?Remove=20AbstractExtension,=20simplify=20?= =?UTF-8?q?constructors=20across=20models,=20and=20replace=20Point=20const?= =?UTF-8?q?ants=20with=20PointType=20enum=20=F0=9F=AB=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- docs/01_Usage/02_Creating_Files.md | 9 +- docs/01_Usage/05_Extensions.md | 3 +- examples/CreateFileFromScratch.php | 5 +- examples/waypoints_create.php | 3 +- src/phpGPX/Models/Bounds.php | 93 +++---- src/phpGPX/Models/Collection.php | 103 ++----- src/phpGPX/Models/Copyright.php | 43 +-- src/phpGPX/Models/Email.php | 33 +-- .../Models/Extensions/AbstractExtension.php | 11 - .../Models/Extensions/ExtensionInterface.php | 2 +- .../Models/Extensions/TrackPointExtension.php | 2 +- src/phpGPX/Models/Link.php | 42 +-- src/phpGPX/Models/Metadata.php | 89 +----- src/phpGPX/Models/Person.php | 48 +--- src/phpGPX/Models/Point.php | 254 ++++-------------- src/phpGPX/Models/Route.php | 17 +- src/phpGPX/Models/Segment.php | 41 +-- src/phpGPX/Models/Stats.php | 30 --- src/phpGPX/Models/Track.php | 16 +- src/phpGPX/Parsers/PointParser.php | 28 +- tests/Integration/GeoJsonOutputTest.php | 13 +- tests/Unit/Analysis/BoundsAnalyzerTest.php | 5 +- tests/Unit/Analysis/EngineTest.php | 7 +- tests/Unit/Analysis/MovementAnalyzerTest.php | 7 +- .../TrackPointExtensionAnalyzerTest.php | 3 +- tests/Unit/Helpers/DistanceCalculatorTest.php | 3 +- .../ElevationGainLossCalculatorTest.php | 5 +- tests/Unit/Helpers/GeoHelperTest.php | 17 +- tests/Unit/Models/StatsCalculationTest.php | 23 +- 30 files changed, 209 insertions(+), 749 deletions(-) delete mode 100644 src/phpGPX/Models/Extensions/AbstractExtension.php diff --git a/README.md b/README.md index f7a59fd..a4b99f7 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ $file->save('output.json', phpGPX::JSON_FORMAT); use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Segment; use phpGPX\Models\Track; use phpGPX\Models\Extensions; @@ -113,7 +114,7 @@ $track->type = 'RUN'; $segment = new Segment(); -$point = new Point(Point::TRACKPOINT); +$point = new Point(PointType::Trackpoint); $point->latitude = 54.9328621088893; $point->longitude = 9.860624216140083; $point->elevation = 0; diff --git a/docs/01_Usage/02_Creating_Files.md b/docs/01_Usage/02_Creating_Files.md index 0d2714f..18ad0a6 100644 --- a/docs/01_Usage/02_Creating_Files.md +++ b/docs/01_Usage/02_Creating_Files.md @@ -8,6 +8,7 @@ You can build GPX files programmatically. use phpGPX\Models\GpxFile; use phpGPX\Models\Metadata; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Segment; use phpGPX\Models\Track; use phpGPX\phpGPX; @@ -34,7 +35,7 @@ $points = [ ]; foreach ($points as $data) { - $point = new Point(Point::TRACKPOINT); + $point = new Point(PointType::Trackpoint); $point->latitude = $data['lat']; $point->longitude = $data['lon']; $point->elevation = $data['ele']; @@ -67,6 +68,7 @@ echo "Distance: " . round($gpxFile->tracks[0]->stats->distance) . " m\n"; ```php use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\phpGPX; @@ -82,7 +84,7 @@ $waypoints = [ ]; foreach ($waypoints as $data) { - $point = new Point(Point::ROUTEPOINT); + $point = new Point(PointType::Routepoint); $point->latitude = $data['lat']; $point->longitude = $data['lon']; $point->elevation = $data['ele']; @@ -100,11 +102,12 @@ $gpxFile->save('trail.gpx', phpGPX::XML_FORMAT); use phpGPX\Models\GpxFile; use phpGPX\Models\Link; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\phpGPX; $gpxFile = new GpxFile(); -$waypoint = new Point(Point::WAYPOINT); +$waypoint = new Point(PointType::Waypoint); $waypoint->latitude = 48.8566; $waypoint->longitude = 2.3522; $waypoint->elevation = 35; diff --git a/docs/01_Usage/05_Extensions.md b/docs/01_Usage/05_Extensions.md index f944281..1e4a812 100644 --- a/docs/01_Usage/05_Extensions.md +++ b/docs/01_Usage/05_Extensions.md @@ -118,10 +118,9 @@ To add support for a new GPX extension type, implement two interfaces: ### 1. Extension model — `ExtensionInterface` ```php -use phpGPX\Models\Extensions\AbstractExtension; use phpGPX\Models\Extensions\ExtensionInterface; -class MyExtension extends AbstractExtension implements ExtensionInterface +class MyExtension implements ExtensionInterface { public const NAMESPACE = 'http://example.com/ext/v1'; public const XSD = 'http://example.com/ext/v1/schema.xsd'; diff --git a/examples/CreateFileFromScratch.php b/examples/CreateFileFromScratch.php index a878bbe..18f5473 100644 --- a/examples/CreateFileFromScratch.php +++ b/examples/CreateFileFromScratch.php @@ -7,6 +7,7 @@ use phpGPX\Models\Link; use phpGPX\Models\Metadata; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Segment; use phpGPX\Models\Track; use phpGPX\Models\Extensions; @@ -84,7 +85,7 @@ foreach ($sample_data as $sample_point) { // Creating trackpoint - $point = new Point(Point::TRACKPOINT); + $point = new Point(PointType::Trackpoint); $point->latitude = $sample_point['latitude']; $point->longitude = $sample_point['longitude']; $point->elevation = $sample_point['elevation']; @@ -106,7 +107,7 @@ $gpx_file->tracks[] = $track; // Create waypoint -$point = new Point(Point::WAYPOINT); +$point = new Point(PointType::Waypoint); $point->name = 'Example Waypoint'; $point->latitude = $sample_point['latitude']; $point->longitude = $sample_point['longitude']; diff --git a/examples/waypoints_create.php b/examples/waypoints_create.php index bfd235e..394f2fb 100644 --- a/examples/waypoints_create.php +++ b/examples/waypoints_create.php @@ -7,6 +7,7 @@ use phpGPX\Models\Link; use phpGPX\Models\Metadata; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Segment; use phpGPX\Models\Track; @@ -75,7 +76,7 @@ $wp = []; foreach ($sample_data as $sample_point) { // Creating trackpoint - $point = new Point(Point::WAYPOINT); + $point = new Point(PointType::Waypoint); $point->latitude = $sample_point['latitude']; $point->longitude = $sample_point['longitude']; $point->elevation = $sample_point['elevation']; diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php index 98f8cf2..3fe1479 100644 --- a/src/phpGPX/Models/Bounds.php +++ b/src/phpGPX/Models/Bounds.php @@ -1,73 +1,40 @@ - */ namespace phpGPX\Models; +/** + * Two lat/lon pairs defining the extent of an element. + */ class Bounds implements \JsonSerializable { - public const TAG_NAME = 'bounds'; - - /** - * Minimal latitude in file. - * @var float|null - */ - public ?float $minLatitude; - - /** - * Minimal longitude in file. - * @var float|null - */ - public ?float $minLongitude; + public const TAG_NAME = 'bounds'; - /** - * Maximal latitude in file. - * @var float|null - */ - public ?float $maxLatitude; + public function __construct( + public ?float $minLatitude = null, + public ?float $minLongitude = null, + public ?float $maxLatitude = null, + public ?float $maxLongitude = null, + ) {} /** - * Maximal longitude in file. - * @var float|null + * GeoJSON bbox: [minLon, minLat, maxLon, maxLat] */ - public ?float $maxLongitude; - - /** - * @param float|null $minLatitude - * @param float|null $minLongitude - * @param float|null $maxLatitude - * @param float|null $maxLongitude - */ - public function __construct(?float $minLatitude, ?float $minLongitude, ?float $maxLatitude, ?float $maxLongitude) - { - $this->minLatitude = $minLatitude; - $this->minLongitude = $minLongitude; - $this->maxLatitude = $maxLatitude; - $this->maxLongitude = $maxLongitude; - } - - /** - * GeoJSON serializer - * @return array - */ - public function jsonSerialize(): array - { - return [$this->minLongitude, $this->minLatitude, $this->maxLongitude, $this->maxLatitude]; - } - - public static function parse(\SimpleXMLElement $node): ?Bounds - { - if ($node->getName() != self::TAG_NAME) { - return null; - } - - return new Bounds( - (float) $node['minlat'], - (float) $node['minlon'], - (float) $node['maxlat'], - (float) $node['maxlon'] - ); - } -} + public function jsonSerialize(): array + { + return [$this->minLongitude, $this->minLatitude, $this->maxLongitude, $this->maxLatitude]; + } + + public static function parse(\SimpleXMLElement $node): ?Bounds + { + if ($node->getName() != self::TAG_NAME) { + return null; + } + + return new Bounds( + (float) $node['minlat'], + (float) $node['minlon'], + (float) $node['maxlat'], + (float) $node['maxlon'] + ); + } +} \ No newline at end of file diff --git a/src/phpGPX/Models/Collection.php b/src/phpGPX/Models/Collection.php index d354fb6..b5c6842 100644 --- a/src/phpGPX/Models/Collection.php +++ b/src/phpGPX/Models/Collection.php @@ -1,100 +1,39 @@ - */ namespace phpGPX\Models; /** - * Class Collection - * @package phpGPX\Models + * Abstract base class for Track and Route. */ abstract class Collection implements \JsonSerializable { + /** GPS name of route / track. */ + public ?string $name = null; - /** - * GPS name of route / track. - * An original GPX 1.1 attribute. - * @var string|null - */ - public ?string $name; - - /** - * GPS comment for route. - * An original GPX 1.1 attribute. - * @var string|null - */ - public ?string $comment; - - /** - * Text description of route/track for user. Not sent to GPS. - * An original GPX 1.1 attribute. - * @var string|null - */ - public ?string $description; - - /** - * Source of data. Included to give user some idea of reliability and accuracy of data. - * An original GPX 1.1 attribute. - * @var string|null - */ - public ?string $source; + /** GPS comment for route. */ + public ?string $comment = null; - /** - * Links to external information about the route/track. - * An original GPX 1.1 attribute. - * @var Link[] - */ - public array $links; + /** Text description of route/track for user. */ + public ?string $description = null; - /** - * GPS route/track number. - * An original GPX 1.1 attribute. - * @var int|null - */ - public ?int $number; + /** Source of data. */ + public ?string $source = null; - /** - * Type (classification) of route/track. - * An original GPX 1.1 attribute. - * @var string|null - */ - public ?string $type; + /** @var Link[] Links to external information. */ + public array $links = []; - /** - * You can add extend GPX by adding your own elements from another schema here. - * An original GPX 1.1 attribute. - * @var Extensions|null - */ - public ?Extensions $extensions; + /** GPS route/track number. */ + public ?int $number = null; - /** - * Objects contains calculated statistics for collection. - * @var Stats|null - */ - public ?Stats $stats; + /** Type (classification) of route/track. */ + public ?string $type = null; - /** - * Collection constructor. - */ - public function __construct() - { - $this->name = null; - $this->comment = null; - $this->description = null; - $this->source = null; - $this->links = []; - $this->number = null; - $this->type = null; - $this->extensions = null; - $this->stats = null; - } + /** GPX extensions. */ + public ?Extensions $extensions = null; + /** Calculated statistics. */ + public ?Stats $stats = null; - /** - * Return all points in collection. - * @return Point[] - */ + /** @return Point[] */ abstract public function getPoints(): array; -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Copyright.php b/src/phpGPX/Models/Copyright.php index f2adb0b..09507ac 100644 --- a/src/phpGPX/Models/Copyright.php +++ b/src/phpGPX/Models/Copyright.php @@ -1,48 +1,17 @@ - */ namespace phpGPX\Models; /** - * Class Copyright * Information about the copyright holder and any license governing use of this file. - * By linking to an appropriate license, you may place your data into the public domain or grant additional usage rights. - * @package phpGPX\Models */ class Copyright implements \JsonSerializable { - - /** - * Copyright holder (TopoSoft, Inc.) - * @var string|null - */ - public ?string $author; - - /** - * Year of copyright. - * @var string|null - */ - public ?string $year; - - /** - * Link to external file containing license text. - * @var string|null - */ - public ?string $license; - - /** - * Copyright constructor. - */ - public function __construct() - { - $this->author = null; - $this->year = null; - $this->license = null; - } - + public function __construct( + public ?string $author = null, + public ?string $year = null, + public ?string $license = null, + ) {} public function jsonSerialize(): array { @@ -52,4 +21,4 @@ public function jsonSerialize(): array 'license' => $this->license, ], fn($v) => $v !== null); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Email.php b/src/phpGPX/Models/Email.php index 32863e1..de290ae 100644 --- a/src/phpGPX/Models/Email.php +++ b/src/phpGPX/Models/Email.php @@ -1,39 +1,16 @@ - */ namespace phpGPX\Models; /** - * Class Email * An email address. Broken into two parts (id and domain) to help prevent email harvesting. - * @package phpGPX\Models */ class Email implements \JsonSerializable { - - /** - * Id half of email address (jakub.dubec) - * @var string|null - */ - public ?string $id = null; - - /** Domain half of email address (gmail.com) - * @var string|null - */ - public ?string $domain = null; - - /** - * Email constructor. - */ - public function __construct() - { - $this->id = null; - $this->domain = null; - } - + public function __construct( + public ?string $id = null, + public ?string $domain = null, + ) {} public function jsonSerialize(): array { @@ -42,4 +19,4 @@ public function jsonSerialize(): array 'domain' => $this->domain, ], fn($v) => $v !== null); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Extensions/AbstractExtension.php b/src/phpGPX/Models/Extensions/AbstractExtension.php deleted file mode 100644 index 843ab14..0000000 --- a/src/phpGPX/Models/Extensions/AbstractExtension.php +++ /dev/null @@ -1,11 +0,0 @@ - - */ - -namespace phpGPX\Models\Extensions; - -abstract class AbstractExtension implements \JsonSerializable -{ -} \ No newline at end of file diff --git a/src/phpGPX/Models/Extensions/ExtensionInterface.php b/src/phpGPX/Models/Extensions/ExtensionInterface.php index a2b3c46..685003d 100644 --- a/src/phpGPX/Models/Extensions/ExtensionInterface.php +++ b/src/phpGPX/Models/Extensions/ExtensionInterface.php @@ -19,7 +19,7 @@ * ## Implementing a custom extension * * ```php - * class MyExtension extends AbstractExtension implements ExtensionInterface + * class MyExtension implements ExtensionInterface * { * public static function getNamespace(): string { return 'http://example.com/ext/v1'; } * public static function getSchemaLocation(): string { return 'http://example.com/ext/v1/schema.xsd'; } diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php index d010545..9b2f452 100644 --- a/src/phpGPX/Models/Extensions/TrackPointExtension.php +++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php @@ -13,7 +13,7 @@ * * @see https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd */ -class TrackPointExtension extends AbstractExtension implements ExtensionInterface +class TrackPointExtension implements ExtensionInterface { const NAMESPACE_URI = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2'; const SCHEMA_LOCATION = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd'; diff --git a/src/phpGPX/Models/Link.php b/src/phpGPX/Models/Link.php index 59ffb62..c3751d0 100644 --- a/src/phpGPX/Models/Link.php +++ b/src/phpGPX/Models/Link.php @@ -1,48 +1,18 @@ - */ namespace phpGPX\Models; /** - * Class Link according to GPX 1.1 specification. * A link to an external resource (Web page, digital photo, video clip, etc) with additional information. * @see http://www.topografix.com/GPX/1/1/#type_linkType - * @package phpGPX\Models */ class Link implements \JsonSerializable { - - /** - * URL of hyperlink. - * @var string|null - */ - public ?string $href = null; - - /** - * Text of hyperlink. - * @var string|null - */ - public ?string $text; - - /** - * Mime type of content (image/jpeg) - * @var string|null - */ - public ?string $type; - - /** - * Link constructor. - */ - public function __construct() - { - $this->href = null; - $this->text = null; - $this->type = null; - } - + public function __construct( + public ?string $href = null, + public ?string $text = null, + public ?string $type = null, + ) {} public function jsonSerialize(): array { @@ -52,4 +22,4 @@ public function jsonSerialize(): array 'type' => $this->type, ], fn($v) => $v !== null); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Metadata.php b/src/phpGPX/Models/Metadata.php index bb3c9cd..57db42b 100644 --- a/src/phpGPX/Models/Metadata.php +++ b/src/phpGPX/Models/Metadata.php @@ -1,97 +1,32 @@ - */ namespace phpGPX\Models; use phpGPX\Helpers\DateTimeHelper; /** - * Class Metadata - * Information about the GPX file, author, and copyright restrictions goes in the metadata section. - * Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. - * @package phpGPX\Models + * Information about the GPX file, author, and copyright restrictions. */ class Metadata implements \JsonSerializable { + public ?string $name = null; - /** - * The name of the GPX file. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $name; - - /** - * A description of the contents of the GPX file. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $description; - - /** - * The person or organization who created the GPX file. - * An original GPX 1.1 attribute. - * @var Person|null - */ - public ?Person $author; + public ?string $description = null; - /** - * Copyright and license information governing use of the file. - * Original GPX 1.1 attribute. - * @var Copyright|null - */ - public ?Copyright $copyright; + public ?Person $author = null; - /** - * Original GPX 1.1 attribute. - * @var Link[]|null - */ - public ?array $links; + public ?Copyright $copyright = null; - /** - * Date of GPX creation - * @var \DateTime|null - */ - public ?\DateTime $time; + /** @var Link[] */ + public array $links = []; - /** - * Keywords associated with the file. Search engines or databases can use this information to classify the data. - * @var string|null - */ - public ?string $keywords; + public ?\DateTime $time = null; - /** - * Minimum and maximum coordinates which describe the extent of the coordinates in the file. - * Original GPX 1.1 attribute. - * @var Bounds|null - */ - public ?Bounds $bounds; + public ?string $keywords = null; - /** - * Extensions. - * @var Extensions|null - */ - public ?Extensions $extensions; - - /** - * Metadata constructor. - */ - public function __construct() - { - $this->name = null; - $this->description = null; - $this->author = null; - $this->copyright = null; - $this->links = []; - $this->time = null; - $this->keywords = null; - $this->bounds = null; - $this->extensions = null; - } + public ?Bounds $bounds = null; + public ?Extensions $extensions = null; public function jsonSerialize(): array { @@ -107,4 +42,4 @@ public function jsonSerialize(): array 'extensions' => $this->extensions, ], fn($v) => $v !== null); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Person.php b/src/phpGPX/Models/Person.php index a0819b5..be0d4ef 100644 --- a/src/phpGPX/Models/Person.php +++ b/src/phpGPX/Models/Person.php @@ -1,50 +1,18 @@ - */ namespace phpGPX\Models; /** - * Class Person - * A person or organisation - * @package phpGPX\Models + * A person or organisation. */ class Person implements \JsonSerializable { - - /** - * Name of person or organization. - * An original GPX 1.1 attribute. - * @var string|null - */ - public ?string $name; - - /** - * E-mail address. - * An original GPX 1.1 attribute. - * @var Email|null - */ - public ?Email $email; - - /** - * Link to Web site or other external information about person. - * An original GPX 1.1 attribute. - * @var Link[]|null - */ - public ?array $links; - - /** - * Person constructor. - */ - public function __construct() - { - $this->name = null; - $this->email = null; - $this->links = null; - } - + public function __construct( + public ?string $name = null, + public ?Email $email = null, + /** @var Link[]|null */ + public ?array $links = null, + ) {} public function jsonSerialize(): array { @@ -54,4 +22,4 @@ public function jsonSerialize(): array 'links' => !empty($this->links) ? $this->links : null, ], fn($v) => $v !== null); } -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index beb6157..fd75679 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -1,8 +1,4 @@ - */ namespace phpGPX\Models; @@ -11,232 +7,91 @@ enum PointType: string { - case waypoint = 'wpt'; - case trackpoint = 'trkpt'; - case routepoint = 'rtept'; + case Waypoint = 'wpt'; + case Trackpoint = 'trkpt'; + case Routepoint = 'rtept'; } /** - * Class Point * GPX point representation according to GPX 1.1 specification. * @see http://www.topografix.com/GPX/1/1/#type_wptType - * @package phpGPX\Models */ class Point implements \JsonSerializable { - const WAYPOINT = 'waypoint'; - const TRACKPOINT = 'track'; - const ROUTEPOINT = 'route'; - - /** - * The latitude of the point. Decimal degrees, WGS84 datum. - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $latitude; - - /** - * The longitude of the point. Decimal degrees, WGS84 datum. - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $longitude; + /** The latitude of the point. Decimal degrees, WGS84 datum. */ + public ?float $latitude = null; - /** - * Elevation (in meters) of the point. - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $elevation; + /** The longitude of the point. Decimal degrees, WGS84 datum. */ + public ?float $longitude = null; - /** - * Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! - * Fractional seconds are allowed for millisecond timing in tracklogs. - * @var \DateTime|null - */ - public ?\DateTime $time; + /** Elevation (in meters) of the point. */ + public ?float $elevation = null; - /** - * Magnetic variation (in degrees) at the point - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $magVar; + /** Creation/modification timestamp (UTC). */ + public ?\DateTime $time = null; - /** - * Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $geoidHeight; + /** Magnetic variation (in degrees) at the point. */ + public ?float $magVar = null; - /** - * The GPS name of the waypoint. This field will be transferred to and from the GPS. - * GPX does not place restrictions on the length of this field or the characters contained in it. - * It is up to the receiving application to validate the field before sending it to the GPS. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $name; + /** Height (in meters) of geoid above WGS84 earth ellipsoid. */ + public ?float $geoidHeight = null; - /** - * GPS waypoint comment. Sent to GPS as comment. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $comment; + /** The GPS name of the waypoint. */ + public ?string $name = null; - /** - * A text description of the element. Holds additional information about the element intended for the user, not the GPS. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $description; + /** GPS waypoint comment. */ + public ?string $comment = null; - /** - * Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $source; + /** Text description of the element. */ + public ?string $description = null; - /** - * Link to additional information about the waypoint. - * Original GPX 1.1 attribute. - * @var Link[] - */ - public array $links; + /** Source of data. */ + public ?string $source = null; - /** - * Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. - * If the GPS abbreviates words, spell them out. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $symbol; + /** @var Link[] Links to additional information about the waypoint. */ + public array $links = []; - /** - * Type (classification) of the waypoint. - * Original GPX 1.1 attribute. - * @var string|null - */ - public ?string $type; + /** Text of GPS symbol name. */ + public ?string $symbol = null; - /** - * Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used - * Possible values: {'none'|'2d'|'3d'|'dgps'|'pps'} - * Original GPX 1.1 attribute. - * @see http://www.topografix.com/GPX/1/1/#type_fixType - * @var string|null - */ - public ?string $fix; + /** Type (classification) of the waypoint. */ + public ?string $type = null; - /** - * Number of satellites used to calculate the GPX fix. Always positive value. - * Original GPX 1.1 attribute. - * @var integer|null - */ - public ?int $satellitesNumber; + /** Type of GPS fix. Possible values: none, 2d, 3d, dgps, pps. */ + public ?string $fix = null; - /** - * Horizontal dilution of precision. - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $hdop; + /** Number of satellites used to calculate the GPX fix. */ + public ?int $satellitesNumber = null; - /** - * Vertical dilution of precision. - * Original GPX 1.1 attribute. - * @var float|null - */ - public ?float $vdop; + /** Horizontal dilution of precision. */ + public ?float $hdop = null; - /** - * Position dilution of precision. - * Original GPX 1.1 attribute - * @var float|null - */ - public ?float $pdop; + /** Vertical dilution of precision. */ + public ?float $vdop = null; - /** - * Number of seconds since last DGPS update. - * Original GPX 1.1 attribute. - * @var integer|null - */ - public ?int $ageOfGpsData; + /** Position dilution of precision. */ + public ?float $pdop = null; - /** - * ID of DGPS station used in differential correction. - * Original GPX 1.1 attribute. - * @see http://www.topografix.com/GPX/1/1/#type_dgpsStationType - * @var integer|null - */ - public ?int $dgpsid; + /** Number of seconds since last DGPS update. */ + public ?int $ageOfGpsData = null; - /** - * Difference in in distance (in meters) between last point. - * Value is created by phpGPX library. - * @var float|null - */ - public ?float $difference; + /** ID of DGPS station used in differential correction. */ + public ?int $dgpsid = null; - /** - * Distance from collection start in meters. - * Value is created by phpGPX library. - * @var float|null - */ - public ?float $distance; + /** Difference in distance (in meters) from previous point. Computed by phpGPX. */ + public ?float $difference = null; - /** - * Objects stores GPX extensions from another namespaces. - * @var Extensions|null - */ - public ?Extensions $extensions; + /** Distance from collection start in meters. Computed by phpGPX. */ + public ?float $distance = null; - /** - * Type of the point (parent collation type (ROUTE|WAYPOINT|TRACK)) - * @var string - */ - private string $pointType; + /** GPX extensions. */ + public ?Extensions $extensions = null; - /** - * Point constructor. - * @param string $pointType - */ - public function __construct(string $pointType) - { - $this->latitude = null; - $this->longitude = null; - $this->elevation = null; - $this->time = null; - $this->magVar = null; - $this->geoidHeight = null; - $this->name = null; - $this->comment = null; - $this->description = null; - $this->source = null; - $this->links = []; - $this->symbol = null; - $this->type = null; - $this->fix = null; - $this->satellitesNumber = null; - $this->hdop = null; - $this->vdop = null; - $this->pdop = null; - $this->ageOfGpsData = null; - $this->dgpsid = null; - $this->difference = null; - $this->distance = null; - $this->extensions = null; - $this->pointType = $pointType; - } + public function __construct( + private readonly PointType $pointType, + ) {} - /** - * Return point type (ROUTE|TRACK|WAYPOINT) - * @return string - */ - public function getPointType(): string + public function getPointType(): PointType { return $this->pointType; } @@ -274,5 +129,4 @@ public function jsonSerialize(): array 'properties' => $properties ?: new \stdClass(), ]; } - -} +} \ No newline at end of file diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 99bfcf2..72d1c18 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -15,21 +15,8 @@ class Route extends Collection { - /** - * A list of route points. - * An original GPX 1.1 attribute. - * @var Point[] - */ - public array $points; - - /** - * Route constructor. - */ - public function __construct() - { - parent::__construct(); - $this->points = []; - } + /** @var Point[] */ + public array $points = []; /** diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index 5468dc2..b83721c 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -1,49 +1,20 @@ - */ namespace phpGPX\Models; use phpGPX\Helpers\SerializationHelper; /** - * Class Segment * A Track Segment holds a list of Track Points which are logically connected in order. - * To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, - * start a new Track Segment for each continuous span of track data. - * @package phpGPX\Models */ class Segment implements \JsonSerializable { - /** - * Array of segment points - * @var Point[] - */ - public array $points; - - /** - * You can add extend GPX by adding your own elements from another schema here. - * @var Extensions|null - */ - public ?Extensions $extensions; + /** @var Point[] */ + public array $points = []; - /** - * @var Stats|null - */ - public ?Stats $stats; - - /** - * Segment constructor. - */ - public function __construct() - { - $this->points = []; - $this->extensions = null; - $this->stats = null; - } + public ?Extensions $extensions = null; + public ?Stats $stats = null; public function jsonSerialize(): array { @@ -67,9 +38,7 @@ public function jsonSerialize(): array ]; } - /** - * @return Point[] - */ + /** @return Point[] */ public function getPoints(): array { return $this->points; diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index 960f671..a312cd1 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -147,36 +147,6 @@ class Stats implements \JsonSerializable */ public ?float $averageTemperature = null; - /** - * Reset all stats - * @return void - */ - public function reset(): void - { - $this->distance = null; - $this->realDistance = null; - $this->averageSpeed = null; - $this->averagePace = null; - $this->minAltitude = null; - $this->maxAltitude = null; - $this->minAltitudeCoords = null; - $this->maxAltitudeCoords = null; - $this->cumulativeElevationGain = null; - $this->cumulativeElevationLoss = null; - $this->startedAt = null; - $this->startedAtCoords = null; - $this->finishedAt = null; - $this->finishedAtCoords = null; - $this->duration = null; - $this->bounds = null; - $this->movingDuration = null; - $this->movingAverageSpeed = null; - $this->averageHeartRate = null; - $this->maxHeartRate = null; - $this->averageCadence = null; - $this->averageTemperature = null; - } - public function jsonSerialize(): array { return array_filter([ diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index 99c29e2..bf2b750 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -15,20 +15,8 @@ class Track extends Collection { - /** - * Array of Track segments - * @var Segment[] - */ - public array $segments; - - /** - * Track constructor. - */ - public function __construct() - { - parent::__construct(); - $this->segments = []; - } + /** @var Segment[] */ + public array $segments = []; /** diff --git a/src/phpGPX/Parsers/PointParser.php b/src/phpGPX/Parsers/PointParser.php index 87b8d9c..7f308d7 100644 --- a/src/phpGPX/Parsers/PointParser.php +++ b/src/phpGPX/Parsers/PointParser.php @@ -1,13 +1,10 @@ - */ namespace phpGPX\Parsers; use phpGPX\Helpers\DateTimeHelper; use phpGPX\Models\Point; +use phpGPX\Models\PointType; abstract class PointParser extends AbstractParser { @@ -95,29 +92,22 @@ protected static function getAttributeMapper(): array ]; } - private static array $typeMapper = [ - 'trkpt' => Point::TRACKPOINT, - 'wpt' => Point::WAYPOINT, - 'rtept' => Point::ROUTEPOINT - ]; - public static function parse(\SimpleXMLElement $node): ?Point { - if (!array_key_exists($node->getName(), self::$typeMapper)) { + $pointType = PointType::tryFrom($node->getName()); + if ($pointType === null) { return null; } - $point = new Point(self::$typeMapper[$node->getName()]); + $point = new Point($pointType); $point->latitude = isset($node['lat']) ? ((float) $node['lat']) : null; $point->longitude = isset($node['lon']) ? ((float) $node['lon']) : null; self::mapAttributesFromXML($node, $point); - // Datetime $point->time = isset($node->time) ? DateTimeHelper::parseDateTime($node->time) : null; - // Delegated parsers $mapper = self::getAttributeMapper(); $point->links = self::parseDelegated($node, 'link', $mapper['link']); $point->extensions = self::parseDelegated($node, 'extensions', $mapper['extensions']); @@ -125,14 +115,9 @@ public static function parse(\SimpleXMLElement $node): ?Point return $point; } - /** - * @param Point $point - * @param \DOMDocument $document - * @return \DOMElement - */ public static function toXML(Point $point, \DOMDocument &$document): \DOMElement { - $node = $document->createElement(array_search($point->getPointType(), self::$typeMapper)); + $node = $document->createElement($point->getPointType()->value); if ($point->latitude !== null) { $node->setAttribute('lat', $point->latitude); @@ -143,18 +128,15 @@ public static function toXML(Point $point, \DOMDocument &$document): \DOMElement self::mapAttributesToXML($point, $document, $node); - // Datetime if ($point->time !== null) { $child = $document->createElement('time', DateTimeHelper::formatDateTime($point->time)); $node->appendChild($child); } - // Delegated parsers $mapper = self::getAttributeMapper(); self::serializeDelegated($point->links, $mapper['link'], $document, $node); self::serializeDelegated($point->extensions, $mapper['extensions'], $document, $node); return $node; } - } \ No newline at end of file diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php index 774f47b..bcfe9d5 100644 --- a/tests/Integration/GeoJsonOutputTest.php +++ b/tests/Integration/GeoJsonOutputTest.php @@ -3,6 +3,7 @@ namespace phpGPX\Tests\Integration; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Track; @@ -32,7 +33,7 @@ public function testGpxFileJsonSerializeIsFeatureCollection(): void public function testWaypointJsonIsPointFeature(): void { - $point = new Point(Point::WAYPOINT); + $point = new Point(PointType::Waypoint); $point->latitude = 49.363; $point->longitude = 0.080; $point->elevation = 100.0; @@ -54,12 +55,12 @@ public function testRouteJsonIsLineStringFeature(): void $route = new Route(); $route->name = 'Test Route'; - $p1 = new Point(Point::ROUTEPOINT); + $p1 = new Point(PointType::Routepoint); $p1->latitude = 54.932; $p1->longitude = 9.860; $p1->elevation = 0.0; - $p2 = new Point(Point::ROUTEPOINT); + $p2 = new Point(PointType::Routepoint); $p2->latitude = 54.933; $p2->longitude = 9.861; $p2->elevation = 1.0; @@ -84,19 +85,19 @@ public function testTrackJsonIsMultiLineStringFeature(): void $track->name = 'Test Track'; $seg1 = new Segment(); - $p1 = new Point(Point::TRACKPOINT); + $p1 = new Point(PointType::Trackpoint); $p1->latitude = 46.571; $p1->longitude = 8.414; $p1->elevation = 2419.0; - $p2 = new Point(Point::TRACKPOINT); + $p2 = new Point(PointType::Trackpoint); $p2->latitude = 46.572; $p2->longitude = 8.415; $p2->elevation = 2420.0; $seg1->points = [$p1, $p2]; $seg2 = new Segment(); - $p3 = new Point(Point::TRACKPOINT); + $p3 = new Point(PointType::Trackpoint); $p3->latitude = 46.573; $p3->longitude = 8.416; $p3->elevation = 2421.0; diff --git a/tests/Unit/Analysis/BoundsAnalyzerTest.php b/tests/Unit/Analysis/BoundsAnalyzerTest.php index 4e94387..5944339 100644 --- a/tests/Unit/Analysis/BoundsAnalyzerTest.php +++ b/tests/Unit/Analysis/BoundsAnalyzerTest.php @@ -6,6 +6,7 @@ use phpGPX\Analysis\Engine; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Stats; @@ -23,7 +24,7 @@ protected function setUp(): void private function makePoint(float $lat, float $lon): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = $lat; $p->longitude = $lon; return $p; @@ -138,7 +139,7 @@ public function testEmptyPointsNoBounds(): void public function testMetadataBoundsIncludesWaypoints(): void { - $waypoint = new Point(Point::WAYPOINT); + $waypoint = new Point(PointType::Waypoint); $waypoint->latitude = 50.0; $waypoint->longitude = 20.0; diff --git a/tests/Unit/Analysis/EngineTest.php b/tests/Unit/Analysis/EngineTest.php index 3a394f0..653a90f 100644 --- a/tests/Unit/Analysis/EngineTest.php +++ b/tests/Unit/Analysis/EngineTest.php @@ -12,6 +12,7 @@ use phpGPX\Analysis\TrackPointExtensionAnalyzer; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Stats; @@ -26,7 +27,7 @@ private function makePoint( ?float $ele = null, ?string $time = null ): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = $lat; $p->longitude = $lon; $p->elevation = $ele; @@ -250,12 +251,12 @@ public function testSortByTimestampSortsRoutePoints(): void { $engine = Engine::default(sortByTimestamp: true); - $p1 = new Point(Point::ROUTEPOINT); + $p1 = new Point(PointType::Routepoint); $p1->latitude = 48.002; $p1->longitude = 17.0; $p1->time = new \DateTime('2024-01-01T10:00:20Z'); - $p2 = new Point(Point::ROUTEPOINT); + $p2 = new Point(PointType::Routepoint); $p2->latitude = 48.000; $p2->longitude = 17.0; $p2->time = new \DateTime('2024-01-01T10:00:00Z'); diff --git a/tests/Unit/Analysis/MovementAnalyzerTest.php b/tests/Unit/Analysis/MovementAnalyzerTest.php index e5638fa..3923d6e 100644 --- a/tests/Unit/Analysis/MovementAnalyzerTest.php +++ b/tests/Unit/Analysis/MovementAnalyzerTest.php @@ -7,6 +7,7 @@ use phpGPX\Analysis\Engine; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Track; @@ -16,7 +17,7 @@ class MovementAnalyzerTest extends TestCase { private function makePoint(float $lat, float $lon, string $time): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = $lat; $p->longitude = $lon; $p->time = new \DateTime($time); @@ -134,11 +135,11 @@ public function testNoTimestampsReturnsNull(): void { $engine = $this->makeEngine(); - $p1 = new Point(Point::TRACKPOINT); + $p1 = new Point(PointType::Trackpoint); $p1->latitude = 48.0; $p1->longitude = 17.0; - $p2 = new Point(Point::TRACKPOINT); + $p2 = new Point(PointType::Trackpoint); $p2->latitude = 48.1; $p2->longitude = 17.1; diff --git a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php index 394451d..e03fdb4 100644 --- a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php +++ b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php @@ -8,6 +8,7 @@ use phpGPX\Models\Extensions\TrackPointExtension; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Track; @@ -29,7 +30,7 @@ private function makePointWithExtension( ?float $cad = null, ?float $aTemp = null ): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = $lat; $p->longitude = $lon; diff --git a/tests/Unit/Helpers/DistanceCalculatorTest.php b/tests/Unit/Helpers/DistanceCalculatorTest.php index 10e8b7c..1439739 100644 --- a/tests/Unit/Helpers/DistanceCalculatorTest.php +++ b/tests/Unit/Helpers/DistanceCalculatorTest.php @@ -5,13 +5,14 @@ use phpGPX\Helpers\DistanceCalculator; use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use PHPUnit\Framework\TestCase; class DistanceCalculatorTest extends TestCase { private function makePoint(float $lat, float $lon, ?float $ele = null): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = $lat; $p->longitude = $lon; $p->elevation = $ele; diff --git a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php index 11c7a92..917c69f 100644 --- a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php +++ b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php @@ -4,13 +4,14 @@ use phpGPX\Helpers\ElevationGainLossCalculator; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use PHPUnit\Framework\TestCase; class ElevationGainLossCalculatorTest extends TestCase { private function makePoint(float $ele): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = 46.57; $p->longitude = 8.41; $p->elevation = $ele; @@ -88,7 +89,7 @@ public function testUpAndDown(): void public function testNullElevationSkipped(): void { $p1 = $this->makePoint(100); - $p2 = new Point(Point::TRACKPOINT); + $p2 = new Point(PointType::Trackpoint); $p2->latitude = 46.57; $p2->longitude = 8.41; $p2->elevation = null; diff --git a/tests/Unit/Helpers/GeoHelperTest.php b/tests/Unit/Helpers/GeoHelperTest.php index 2af5ea6..4127a5a 100644 --- a/tests/Unit/Helpers/GeoHelperTest.php +++ b/tests/Unit/Helpers/GeoHelperTest.php @@ -4,6 +4,7 @@ use phpGPX\Helpers\GeoHelper; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use PHPUnit\Framework\TestCase; class GeoHelperTest extends TestCase @@ -17,11 +18,11 @@ class GeoHelperTest extends TestCase */ public function testGetDistance(): void { - $point1 = new Point(Point::WAYPOINT); + $point1 = new Point(PointType::Waypoint); $point1->latitude = 48.1573923225717; $point1->longitude = 17.0547121910204; - $point2 = new Point(Point::WAYPOINT); + $point2 = new Point(PointType::Waypoint); $point2->latitude = 48.1644916381763; $point2->longitude = 17.0591753907502; @@ -38,12 +39,12 @@ public function testGetDistance(): void */ public function testRealDistance(): void { - $point1 = new Point(Point::WAYPOINT); + $point1 = new Point(PointType::Waypoint); $point1->latitude = 48.1573923225717; $point1->longitude = 17.0547121910204; $point1->elevation = 100; - $point2 = new Point(Point::WAYPOINT); + $point2 = new Point(PointType::Waypoint); $point2->latitude = 48.1644916381763; $point2->longitude = 17.0591753907502; $point2->elevation = 200; @@ -65,11 +66,11 @@ public function testRealDistance(): void public function testSamePointZeroDistance(): void { - $point1 = new Point(Point::WAYPOINT); + $point1 = new Point(PointType::Waypoint); $point1->latitude = 48.1573923225717; $point1->longitude = 17.0547121910204; - $point2 = new Point(Point::WAYPOINT); + $point2 = new Point(PointType::Waypoint); $point2->latitude = 48.1573923225717; $point2->longitude = 17.0547121910204; @@ -78,11 +79,11 @@ public function testSamePointZeroDistance(): void public function testRealDistanceWithNullElevation(): void { - $point1 = new Point(Point::WAYPOINT); + $point1 = new Point(PointType::Waypoint); $point1->latitude = 48.1573923225717; $point1->longitude = 17.0547121910204; - $point2 = new Point(Point::WAYPOINT); + $point2 = new Point(PointType::Waypoint); $point2->latitude = 48.1644916381763; $point2->longitude = 17.0591753907502; diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php index cbf616e..1296beb 100644 --- a/tests/Unit/Models/StatsCalculationTest.php +++ b/tests/Unit/Models/StatsCalculationTest.php @@ -9,6 +9,7 @@ use phpGPX\Analysis\TimestampAnalyzer; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; +use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; use phpGPX\Models\Stats; @@ -34,7 +35,7 @@ private function makePoint( ?float $ele = null, ?string $time = null ): Point { - $p = new Point(Point::TRACKPOINT); + $p = new Point(PointType::Trackpoint); $p->latitude = $lat; $p->longitude = $lon; $p->elevation = $ele; @@ -48,7 +49,7 @@ private function makeRoutePoint( ?float $ele = null, ?string $time = null ): Point { - $p = new Point(Point::ROUTEPOINT); + $p = new Point(PointType::Routepoint); $p->latitude = $lat; $p->longitude = $lon; $p->elevation = $ele; @@ -347,24 +348,6 @@ public function testRouteStatsBasic(): void // --- Stats model --- - public function testStatsReset(): void - { - $stats = new Stats(); - $stats->distance = 100.0; - $stats->duration = 60.0; - $stats->averageSpeed = 1.67; - $stats->cumulativeElevationGain = 50.0; - - $stats->reset(); - - $this->assertNull($stats->distance); - $this->assertNull($stats->duration); - $this->assertNull($stats->averageSpeed); - $this->assertNull($stats->cumulativeElevationGain); - $this->assertNull($stats->startedAt); - $this->assertNull($stats->finishedAt); - } - public function testStatsJsonSerialize(): void { $stats = new Stats(); From 36067a5db768821a6e5609ff55066b6ee8a89a0e Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 00:05:30 +0100 Subject: [PATCH 22/31] =?UTF-8?q?Split=20test=20suites=20into=20unit=20and?= =?UTF-8?q?=20integration=20for=20separate=20coverage=20reporting=20in=20C?= =?UTF-8?q?odecov=20=F0=9F=A4=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 20 ++++++++++++++++---- codecov.yml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff529a8..ac36d0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,13 +45,25 @@ jobs: - name: Install dependencies run: composer install --no-interaction --prefer-dist - - name: Run tests with coverage - run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --coverage-clover coverage.xml + - name: Run unit tests with coverage + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --testsuite unit --coverage-clover coverage-unit.xml - - name: Upload coverage to Codecov + - name: Run integration tests with coverage + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --testsuite integration --coverage-clover coverage-integration.xml + + - name: Upload unit coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage-unit.xml + flags: unit + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload integration coverage to Codecov uses: codecov/codecov-action@v4 with: - files: coverage.xml + files: coverage-integration.xml + flags: integration env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..73b031c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,31 @@ +codecov: + require_ci_to_pass: true + +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: 80% + +flag_management: + default_rules: + carryforward: true + individual_flags: + - name: unit + paths: + - src/ + statuses: + - type: project + target: auto + threshold: 1% + - name: integration + paths: + - src/ + statuses: + - type: project + target: auto + threshold: 1% \ No newline at end of file From 29a344f68d280a2aa3090b3bca900b27cf12ccde Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 00:08:33 +0100 Subject: [PATCH 23/31] Move PointType enum to its own file (PSR-4 autoloading) --- src/phpGPX/Models/Point.php | 7 ------- src/phpGPX/Models/PointType.php | 10 ++++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 src/phpGPX/Models/PointType.php diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index fd75679..0dd66cf 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -5,13 +5,6 @@ use phpGPX\Helpers\DateTimeHelper; use phpGPX\Helpers\SerializationHelper; -enum PointType: string -{ - case Waypoint = 'wpt'; - case Trackpoint = 'trkpt'; - case Routepoint = 'rtept'; -} - /** * GPX point representation according to GPX 1.1 specification. * @see http://www.topografix.com/GPX/1/1/#type_wptType diff --git a/src/phpGPX/Models/PointType.php b/src/phpGPX/Models/PointType.php new file mode 100644 index 0000000..2fa4697 --- /dev/null +++ b/src/phpGPX/Models/PointType.php @@ -0,0 +1,10 @@ + Date: Mon, 9 Mar 2026 00:13:50 +0100 Subject: [PATCH 24/31] =?UTF-8?q?add=20JUnit=20test=20result=20uploads=20t?= =?UTF-8?q?o=20Codecov=20=F0=9F=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac36d0e..7041e49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,12 +46,14 @@ jobs: run: composer install --no-interaction --prefer-dist - name: Run unit tests with coverage - run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --testsuite unit --coverage-clover coverage-unit.xml + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --testsuite unit --coverage-clover coverage-unit.xml --log-junit junit-unit.xml - name: Run integration tests with coverage - run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --testsuite integration --coverage-clover coverage-integration.xml + if: ${{ !cancelled() }} + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.xml --testsuite integration --coverage-clover coverage-integration.xml --log-junit junit-integration.xml - name: Upload unit coverage to Codecov + if: ${{ !cancelled() }} uses: codecov/codecov-action@v4 with: files: coverage-unit.xml @@ -60,6 +62,7 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Upload integration coverage to Codecov + if: ${{ !cancelled() }} uses: codecov/codecov-action@v4 with: files: coverage-integration.xml @@ -67,6 +70,24 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Upload unit test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + files: junit-unit.xml + flags: unit + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload integration test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + files: junit-integration.xml + flags: integration + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + docs: runs-on: ubuntu-latest needs: test From 1c440b85ed162736c03f33319aa2dfb805e5453f Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 03:14:26 +0100 Subject: [PATCH 25/31] =?UTF-8?q?Moving=20to=20mkdocs-material=20because?= =?UTF-8?q?=20I=20hated=20all=20other=20php=20alternatives=20-=20I=20am=20?= =?UTF-8?q?sorry=20=F0=9F=98=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 25 -- .github/workflows/docs.yml | 54 ++++ .gitignore | 4 +- composer.json | 5 +- docs/00_Getting_Started/_index.md | 3 - docs/01_Usage/_index.md | 3 - docs/02_Output_Formats/03_GeoJSON.md | 5 - docs/02_Output_Formats/_index.md | 3 - docs/03_API_Reference/_index.md | 11 - docs/04_Development/03_Roadmap_2x.md | 283 ------------------ docs/04_Development/_index.md | 3 - docs/config.json | 6 - .../contributing.md} | 0 docs/development/migration.md | 245 +++++++++++++++ .../stats-architecture.md} | 0 .../02_Testing.md => development/testing.md} | 0 .../installation.md} | 0 .../quick-start.md} | 0 .../02_JSON.md => output-formats/json.md} | 0 .../01_XML.md => output-formats/xml.md} | 0 .../configuration.md} | 2 +- .../creating-files.md} | 0 .../05_Extensions.md => usage/extensions.md} | 4 +- .../loading-files.md} | 2 +- .../03_Statistics.md => usage/statistics.md} | 0 examples/Example.php | 22 +- examples/waypoints_load.php | 12 +- mkdocs.yml | 66 ++++ phpdoc.xml | 6 - 29 files changed, 397 insertions(+), 367 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/00_Getting_Started/_index.md delete mode 100644 docs/01_Usage/_index.md delete mode 100644 docs/02_Output_Formats/03_GeoJSON.md delete mode 100644 docs/02_Output_Formats/_index.md delete mode 100644 docs/03_API_Reference/_index.md delete mode 100644 docs/04_Development/03_Roadmap_2x.md delete mode 100644 docs/04_Development/_index.md delete mode 100644 docs/config.json rename docs/{04_Development/01_Contributing.md => development/contributing.md} (100%) create mode 100644 docs/development/migration.md rename docs/{04_Development/04_Stats_Architecture.md => development/stats-architecture.md} (100%) rename docs/{04_Development/02_Testing.md => development/testing.md} (100%) rename docs/{00_Getting_Started/01_Installation.md => getting-started/installation.md} (100%) rename docs/{00_Getting_Started/02_Quick_Start.md => getting-started/quick-start.md} (100%) rename docs/{02_Output_Formats/02_JSON.md => output-formats/json.md} (100%) rename docs/{02_Output_Formats/01_XML.md => output-formats/xml.md} (100%) rename docs/{01_Usage/04_Configuration.md => usage/configuration.md} (98%) rename docs/{01_Usage/02_Creating_Files.md => usage/creating-files.md} (100%) rename docs/{01_Usage/05_Extensions.md => usage/extensions.md} (97%) rename docs/{01_Usage/01_Loading_Files.md => usage/loading-files.md} (98%) rename docs/{01_Usage/03_Statistics.md => usage/statistics.md} (100%) create mode 100644 mkdocs.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7041e49..32f397e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,28 +87,3 @@ jobs: flags: integration env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - docs: - runs-on: ubuntu-latest - needs: test - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: dom, simplexml, libxml - - - name: Install phpDocumentor - run: curl -sL https://phpdoc.org/phpDocumentor.phar -o /usr/local/bin/phpdoc && chmod +x /usr/local/bin/phpdoc - - - name: Build docs - run: phpdoc run --config phpdoc.xml - - - name: Upload docs artifact - uses: actions/upload-artifact@v4 - with: - name: docs - path: docs-output/ \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c1327ad --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,54 @@ +name: Docs + +on: + push: + branches: [develop] + paths: + - 'docs/**' + - 'mkdocs.yml' + workflow_dispatch: + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install mkdocs-material + run: pip install mkdocs-material + + - name: Build and deploy to gh-pages (develop/) + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Fetch gh-pages if it exists + git fetch origin gh-pages:gh-pages 2>/dev/null || true + + # Build the site + mkdocs build --strict --site-dir site + + # Check out gh-pages (or create orphan) + if git rev-parse --verify gh-pages >/dev/null 2>&1; then + git checkout gh-pages + else + git checkout --orphan gh-pages + git rm -rf . + fi + + # Clear develop/ subdirectory and copy new build + rm -rf develop/ + cp -r site/ develop/ + + # Commit and push + git add develop/ + git diff --cached --quiet || (git commit -m "Deploy docs (develop) from ${GITHUB_SHA::8}" && git push origin gh-pages) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7d9decf..d5dbd6c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ composer.lock /.phpunit.cache coverage.xml /docs-output/ -/.phpdoc/ \ No newline at end of file +/.phpdoc/ +/site/ +/.venv/ \ No newline at end of file diff --git a/composer.json b/composer.json index 33b709c..57e9c5b 100644 --- a/composer.json +++ b/composer.json @@ -39,6 +39,9 @@ "scripts": { "test": "phpunit", "test:unit": "phpunit --testsuite unit", - "test:integration": "phpunit --testsuite integration" + "test:integration": "phpunit --testsuite integration", + "docs:build": "mkdocs build --strict", + "docs:serve": "mkdocs serve", + "cs-fix": "php-cs-fixer fix --config .php-cs-fixer.php" } } diff --git a/docs/00_Getting_Started/_index.md b/docs/00_Getting_Started/_index.md deleted file mode 100644 index 9dcecf8..0000000 --- a/docs/00_Getting_Started/_index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Getting Started - -This section covers installation and first steps with phpGPX. \ No newline at end of file diff --git a/docs/01_Usage/_index.md b/docs/01_Usage/_index.md deleted file mode 100644 index fa186b1..0000000 --- a/docs/01_Usage/_index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Usage - -Detailed guides on phpGPX features. \ No newline at end of file diff --git a/docs/02_Output_Formats/03_GeoJSON.md b/docs/02_Output_Formats/03_GeoJSON.md deleted file mode 100644 index 516ea0f..0000000 --- a/docs/02_Output_Formats/03_GeoJSON.md +++ /dev/null @@ -1,5 +0,0 @@ -# GeoJSON - -See [JSON (GeoJSON)](02_JSON.md) — in phpGPX 2.x, JSON output is always GeoJSON (RFC 7946). - -Both `phpGPX::JSON_FORMAT` and `phpGPX::GEOJSON_FORMAT` produce the same GeoJSON `FeatureCollection` output. \ No newline at end of file diff --git a/docs/02_Output_Formats/_index.md b/docs/02_Output_Formats/_index.md deleted file mode 100644 index 9f6dd19..0000000 --- a/docs/02_Output_Formats/_index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Output Formats - -phpGPX supports three output formats: XML (GPX), JSON, and GeoJSON. \ No newline at end of file diff --git a/docs/03_API_Reference/_index.md b/docs/03_API_Reference/_index.md deleted file mode 100644 index fa5cb2c..0000000 --- a/docs/03_API_Reference/_index.md +++ /dev/null @@ -1,11 +0,0 @@ -# API Reference - -The API reference is auto-generated from PHPDoc comments by [phpDocumentor](https://www.phpdoc.org/). - -Build the full documentation site (API reference + guides) with: - -```bash -composer run docs:build -``` - -The output goes to `docs-output/`. API class pages are under `docs-output/classes/`. \ No newline at end of file diff --git a/docs/04_Development/03_Roadmap_2x.md b/docs/04_Development/03_Roadmap_2x.md deleted file mode 100644 index 427c0c9..0000000 --- a/docs/04_Development/03_Roadmap_2x.md +++ /dev/null @@ -1,283 +0,0 @@ -# Roadmap: phpGPX 2.x - -This document tracks the architectural plan and implementation phases for the phpGPX 2.0 release. -The `develop` branch is the home of all 2.x work. - -## Design Principles - -- **PHP 8.1+ only** — leverage enums, readonly properties, typed properties, union types -- **External parsers** — models stay clean; XML serialization lives in Parser classes (Data Mapper pattern) -- **Middleware pipeline** — replaces static config flags with composable, pluggable processing -- **GeoJSON-native JSON output** — `JsonSerializable` on models returns GeoJSON (RFC 7946) -- **Nullable properties** — GPX files with missing attributes are not rejected - ---- - -## Completed - -### Pre-Phase - -- [x] Drop PHP < 8.1 support -- [x] Upgrade to PHPUnit 10+ (supports 10.5, 11.x, 12.x) -- [x] Remove `Summarizable` interface and `toArray()` — replaced by `JsonSerializable` (#69) -- [x] Remove `GpxSerializable` interface — dead code, parsers handle XML serialization -- [x] Standardize test fixture directory naming -- [x] `PointType` enum (replaces string constants for point type mapping) - -### Phase 1: Parser Consolidation - -- [x] **1.1 — AbstractParser base class** - Extracted `AbstractParser` with four methods: `mapAttributesFromXML()`, `mapAttributesToXML()`, - `parseDelegated()`, `serializeDelegated()`. Five parsers refactored to extend it: - TrackParser, RouteParser, PointParser, MetadataParser, TrackPointExtensionParser. - Remaining 8 parsers (SegmentParser, LinkParser, PersonParser, EmailParser, CopyrightParser, - BoundsParser, ExtensionParser, WaypointParser) are standalone — they are too small or too - specialized to benefit from the base class. - -- [x] **1.2 — Return type declarations on all parser methods** - All `parse()` and `toXML()` methods across all 13 parsers now have explicit return types. - All `$tagName` properties typed as `string`. - -- [x] **1.3 — Unified `$attributeMapper` format** - Attribute mappers in refactored parsers use a `'parser'` key for delegated parsing and - `'datetime'` type for DateTime fields. All switch/case blocks for delegation eliminated. - Unified parser contract: every `parse()` accepts a single `SimpleXMLElement` node and - returns a single model object (or null). Collection iteration is handled by - `AbstractParser::parseDelegated()` based on `'type' => 'array'`. - SegmentParser and LinkParser standardized to single-node contract. - Removed all `toXMLArray` methods from refactored parsers — iteration handled by - `serializeDelegated()`. - ---- - -### Phase 2: Instance-Based `phpGPX` Entry Point - -- [x] **2.1 — `Config` value object** - Created `Config` class with constructor promotion. All settings are explicit, typed, documented. - Removed `datetimeFormat` and `datetimeTimezone` — GeoJSON always outputs ISO 8601 UTC - per industry convention (RFC 7946, Mapbox, GDAL). Datetime formatting is a consumer concern. - - Final Config properties (9): - `calculateStats`, `sortByTimestamp`, `prettyPrint`, `ignoreZeroElevation`, - `applyElevationSmoothing`, `elevationSmoothingThreshold`, `elevationSmoothingSpikesThreshold`, - `applyDistanceSmoothing`, `distanceSmoothingThreshold`. - -- [x] **2.2 — Instance-based `phpGPX` class (#68)** - Rewrote `phpGPX` as an instance class. No static properties. `load()` and `parse()` are - instance methods. Config passed via constructor: `new phpGPX(new Config(...))`. - Only `getSignature()` and format constants remain static (stateless). - No legacy static bridge — clean break from 1.x. - -- [x] **2.3 — Stats calculation moved out of parsers** - Removed `recalculateStats()` calls from TrackParser, RouteParser, SegmentParser. - Parsers only produce the model tree from XML — no side effects. - `phpGPX::parse()` handles post-processing in two steps: - 1. `sortByTimestamp` — sorts point arrays in-place via `DateTimeHelper::comparePointsByTimestamp` - 2. `calculateStats` — calls `recalculateStats(Config)` on each track and route - -- [x] **2.4 — Config threaded through entire model/helper chain** - `StatsCalculator::recalculateStats(Config $config)` — interface updated. - `Segment`, `Route`, `Track` — `recalculateStats()` accepts Config, passes it to: - - `ElevationGainLossCalculator::calculate(points, config)` — uses `ignoreZeroElevation`, - `applyElevationSmoothing`, `elevationSmoothingThreshold`, `elevationSmoothingSpikesThreshold` - - `DistanceCalculator::__construct(points, config)` — uses `applyDistanceSmoothing`, - `distanceSmoothingThreshold` - - Min/max altitude loops use `config->ignoreZeroElevation` - `GpxFile` — holds Config via constructor, uses `prettyPrint` for XML/JSON output. - `Point`, `Stats` — no Config needed. DateTime serialization uses hardcoded ISO 8601 UTC defaults. - -- [x] **2.5 — All tests updated** - Unit tests (DistanceCalculatorTest, ElevationGainLossCalculatorTest, StatsCalculationTest) - use `new Config(...)` with named arguments instead of static property mutation. - Integration tests (GpxFileLoadTest, XmlRoundTripTest, GeoJsonOutputTest) use `new phpGPX()`. - 86 tests, 356 assertions — all passing. - - Config property verification — every property is declared, used, and tested: - | Property | Consumer | - |---|---| - | `calculateStats` | `phpGPX::parse()` | - | `sortByTimestamp` | `phpGPX::sortPointsByTimestamp()` | - | `prettyPrint` | `GpxFile::toJSON()`, `GpxFile::toXML()` | - | `ignoreZeroElevation` | `ElevationGainLossCalculator`, `Segment/Route::recalculateStats()` | - | `applyElevationSmoothing` | `ElevationGainLossCalculator` | - | `elevationSmoothingThreshold` | `ElevationGainLossCalculator` | - | `elevationSmoothingSpikesThreshold` | `ElevationGainLossCalculator` | - | `applyDistanceSmoothing` | `DistanceCalculator` | - | `distanceSmoothingThreshold` | `DistanceCalculator` | - ---- - -## Phase 3: Middleware System (#68) - -**Goal:** Composable post-parse processing pipeline for features that go beyond Config flags. - -> **Note:** Phase 2 already eliminated all static config properties and replaced the core -> processing flags (stats, smoothing, sorting) with the `Config` value object. Middleware -> is for new, composable features that don't fit as simple boolean flags. - -### 3.1 — Define `MiddlewareInterface` - -```php -interface MiddlewareInterface -{ - public function process(GpxFile $gpxFile, Config $config): GpxFile; -} -``` - -### 3.2 — Implement middlewares for new features - -| Middleware | Purpose | GitHub Issue | -|---|---|---| -| `BoundsMiddleware` | Auto-compute coordinate bounds for tracks/routes/segments | #28 | -| `TrackPointExtensionStatsMiddleware` | Aggregate stats from extension data (HR, cadence, power) | #15 | -| `MovementDurationMiddleware` | Exclude pauses from duration/speed calculations | Discussion #73 | - -### 3.3 — Middleware pipeline in `phpGPX` - -```php -$gpx = new phpGPX(); -$gpx->addMiddleware(new BoundsMiddleware()); -$gpx->addMiddleware(new MovementDurationMiddleware(pauseThreshold: 30)); -$file = $gpx->load('track.gpx'); -``` - -Middlewares run after parsing and after built-in Config-driven processing (sorting, stats). - ---- - -## Phase 4: Universal Extension Processing (#41) - -**Goal:** Make it easy to add new GPX extension types without modifying core code. - -### 4.1 — `ExtensionInterface` contract - -```php -interface ExtensionInterface extends \JsonSerializable -{ - public static function getNamespace(): string; - public static function getNamespacePrefix(): string; - public static function getSchemaLocation(): string; - public static function getElementName(): string; -} -``` - -### 4.2 — Extension registry - -Replace the hardcoded `TrackPointExtension` check in `ExtensionParser` with a registry: - -```php -$gpx = new phpGPX(); -$gpx->registerExtension(TrackPointExtension::class, TrackPointExtensionParser::class); -$gpx->registerExtension(StyleExtension::class, StyleExtensionParser::class); // PR #75 -``` - -`TrackPointExtension` stays registered by default. Third-party extensions can be added -without modifying library code. - -### 4.3 — Add GPX version attribute support (#72) - -Parse and preserve the GPX `version` attribute on `GpxFile`. - ---- - -## Phase 5: Strict Typing & Model Cleanup - -**Goal:** Full typed codebase, clean model hierarchy. - -### 5.1 — Constructor promotion for simple models - -```php -// Before -class Bounds implements \JsonSerializable { - public ?float $minLatitude; - // ... - public function __construct(?float $minLatitude, ...) { - $this->minLatitude = $minLatitude; - } -} - -// After -class Bounds implements \JsonSerializable { - public function __construct( - public ?float $minLatitude = null, - public ?float $minLongitude = null, - public ?float $maxLatitude = null, - public ?float $maxLongitude = null, - ) {} -} -``` - -Apply to: Bounds, Email, Copyright, Link, Person, Extensions, TrackPointExtension. - -### 5.2 — Replace `Point` string constants with `PointType` enum usage - -`Point::WAYPOINT`, `Point::TRACKPOINT`, `Point::ROUTEPOINT` constants still exist alongside -the `PointType` enum. Remove the string constants, use the enum everywhere (parsers, tests). - -### 5.3 — `Stats` as a value object - -Consider making `Stats` immutable — constructed by `StatsMiddleware`, not mutated in-place. -The `reset()` + incremental mutation pattern is fragile. A builder or factory approach -within the middleware is cleaner. - -### 5.4 — Evaluate `StatsCalculator` interface on models - -Stats computation is currently driven by `phpGPX::parse()` calling `recalculateStats(Config)` -on models. Consider whether the interface should remain (allows manual re-calculation by users) -or be removed in favor of a standalone stats service. `getPoints()` can stay as a convenience -method on `Collection` without being interface-mandated. - -### 5.5 — Fix startedAt/finishedAt for missing timestamps (#51) - -Ensure the stats middleware correctly scans for the first/last non-null timestamp -rather than assuming boundary points have timestamps. (Partially addressed in current code, -verify with dedicated test cases.) - ---- - -## Phase 6: Documentation & Release - -### 6.1 — Fix documentation generation (#76) - -Evaluate phpDocumentor vs alternatives. Set up automated doc builds in CI. - -### 6.2 — Migration guide (1.x → 2.x) - -Document all breaking changes: -- Removed `Summarizable` / `toArray()` — use `jsonSerialize()` -- Removed `GpxSerializable` -- Instance-based API: `new phpGPX()` instead of static `phpGPX::load()` -- `Config` value object instead of static config flags (`phpGPX::$CALCULATE_STATS`, etc.) -- `recalculateStats(Config $config)` — requires Config parameter -- JSON output is GeoJSON (RFC 7946) — no configurable datetime format (always ISO 8601 UTC) -- Middleware system for extensible post-processing -- Extension registry vs hardcoded extensions -- `PointType` enum vs string constants - -### 6.3 — Update all existing docs - -Rewrite Getting Started, Usage, Configuration, Extensions sections to reflect 2.x API. - -### 6.4 — Performance benchmarks (1.x vs 2.x) - -Benchmark parsing and serialization of large GPX files. Ensure no regressions -from the architectural changes. - ---- - -## Issue Tracker Cross-Reference - -| Issue | Title | Phase | -|---|---|---| -| #15 | Create statistics from GPX extensions | Phase 3 (TrackPointExtensionStatsMiddleware) | -| #28 | Statistics - get Bounds of GPX Routes | Phase 3 (BoundsMiddleware) | -| #41 | Implementing waypoint and creation time extensions | Phase 4 (Extension registry) | -| #51 | startedAt/finishedAt missing timestamps | Phase 5.5 | -| #59 | Elevation gain/loss accuracy | Completed (Config-driven smoothing in Phase 2) | -| #68 | Middlewares | Phase 2 (Config) + Phase 3 (pipeline) | -| #69 | Removal of Summarizable and toArray | Completed | -| #70 | Min altitude not necessarily first point | Completed (verify) | -| #72 | Add GPX version attribute | Phase 4.3 | -| #73 | Movement duration statistics (discussion) | Phase 3 (MovementDurationMiddleware) | -| #75 | Style extension (draft PR) | Phase 4 (Extension registry) | -| #76 | Fix documentation generation | Phase 6.1 | \ No newline at end of file diff --git a/docs/04_Development/_index.md b/docs/04_Development/_index.md deleted file mode 100644 index 22b8154..0000000 --- a/docs/04_Development/_index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Development - -Information for contributors and developers. \ No newline at end of file diff --git a/docs/config.json b/docs/config.json deleted file mode 100644 index eb1bba7..0000000 --- a/docs/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "phpGPX", - "tagline": "A PHP library for reading and creating GPX files", - "author": "Jakub Dubec", - "repo": "https://github.com/Sibyx/phpGPX" -} \ No newline at end of file diff --git a/docs/04_Development/01_Contributing.md b/docs/development/contributing.md similarity index 100% rename from docs/04_Development/01_Contributing.md rename to docs/development/contributing.md diff --git a/docs/development/migration.md b/docs/development/migration.md new file mode 100644 index 0000000..ab6a29d --- /dev/null +++ b/docs/development/migration.md @@ -0,0 +1,245 @@ +# Migration Guide: 1.x to 2.x + +This guide covers all breaking changes when upgrading from phpGPX 1.x to 2.x. + +## Requirements + +- **PHP 8.1+** (was 7.1+ in early 1.x, 7.4+ in later releases) + +--- + +## 1. Instance-Based API + +The static entry point is gone. All interaction goes through an instance. + +**Before (1.x):** +```php +use phpGPX\phpGPX; + +phpGPX::$PRETTY_PRINT = true; +phpGPX::$CALCULATE_STATS = true; + +$file = phpGPX::load('track.gpx'); +``` + +**After (2.x):** +```php +use phpGPX\phpGPX; +use phpGPX\Config; +use phpGPX\Analysis\Engine; + +$gpx = new phpGPX( + config: new Config(prettyPrint: true), + engine: Engine::default(), +); +$file = $gpx->load('track.gpx'); +``` + +No engine = no stats. Pass `Engine::default()` to compute statistics on load. + +--- + +## 2. Configuration + +All static config properties are removed. `Config` is now a value object with a single output concern. + +| 1.x Static Property | 2.x Equivalent | +|---|---| +| `phpGPX::$PRETTY_PRINT` | `new Config(prettyPrint: true)` | +| `phpGPX::$CALCULATE_STATS` | Pass `engine: Engine::default()` (omit for no stats) | +| `phpGPX::$APPLY_ELEVATION_SMOOTHING` | `Engine::default(applyElevationSmoothing: true)` | +| `phpGPX::$ELEVATION_SMOOTHING_THRESHOLD` | `Engine::default(elevationSmoothingThreshold: 3)` | +| `phpGPX::$ELEVATION_SMOOTHING_SPIKES_THRESHOLD` | `Engine::default(elevationSmoothingSpikesThreshold: 50)` | +| `phpGPX::$APPLY_DISTANCE_SMOOTHING` | `Engine::default(applyDistanceSmoothing: true)` | +| `phpGPX::$DISTANCE_SMOOTHING_THRESHOLD` | `Engine::default(distanceSmoothingThreshold: 5)` | +| `phpGPX::$DATETIME_FORMAT` | Removed — always ISO 8601 UTC | +| `phpGPX::$DATETIME_TIMEZONE` | Removed — always UTC | + +Processing options now live on analyzer constructors, accessed via `Engine::default(...)` named arguments. + +--- + +## 3. Stats Calculation + +Models no longer compute their own statistics. The `Engine` does it in a single pass. + +**Before (1.x):** +```php +$track->recalculateStats(); +$segment->recalculateStats(); +``` + +**After (2.x):** +```php +// Option A: Engine runs automatically on load +$gpx = new phpGPX(engine: Engine::default()); +$file = $gpx->load('track.gpx'); +// $file->tracks[0]->stats is populated + +// Option B: Run engine manually +$file = (new phpGPX())->load('track.gpx'); +$file = Engine::default()->process($file); +``` + +**Removed:** +- `recalculateStats()` on Track, Segment, Route +- `StatsCalculator` interface +- `Stats::reset()` + +**New Stats fields** (all nullable): + +| Field | Type | Description | +|---|---|---| +| `bounds` | `Bounds` | Coordinate bounding box | +| `movingDuration` | `float` | Duration excluding stops (seconds) | +| `movingAverageSpeed` | `float` | Speed while moving (m/s) | +| `averageHeartRate` | `float` | From TrackPointExtension (bpm) | +| `maxHeartRate` | `float` | From TrackPointExtension (bpm) | +| `averageCadence` | `float` | From TrackPointExtension (rpm) | +| `averageTemperature` | `float` | From TrackPointExtension (C) | +| `minAltitudeCoords` | `array` | Coordinate at min altitude | +| `maxAltitudeCoords` | `array` | Coordinate at max altitude | +| `startedAtCoords` | `array` | Coordinate at start time | +| `finishedAtCoords` | `array` | Coordinate at end time | + +--- + +## 4. JSON Output is GeoJSON + +JSON output now conforms to GeoJSON (RFC 7946). The structure has changed. + +**Before (1.x):** +```php +$array = $track->stats->toArray(); +$json = $file->toJSON(); // Custom JSON format +``` + +**After (2.x):** +```php +$array = $track->stats->jsonSerialize(); +$json = $file->toJSON(); // GeoJSON FeatureCollection +``` + +**Removed:** +- `Summarizable` interface +- `toArray()` on all models + +GeoJSON geometry mapping: +- Tracks → `Feature` with `MultiLineString` +- Routes → `Feature` with `LineString` +- Waypoints → `Feature` with `Point` + +DateTime is always serialized as ISO 8601 UTC. There is no configurable format. + +--- + +## 5. Extensions + +The extension system is completely rewritten with a registry-based approach. + +**Before (1.x):** +```php +$ext = $point->extensions->trackPointExtension; +$ext->hr; // heart rate +``` + +**After (2.x):** +```php +use phpGPX\Models\Extensions\TrackPointExtension; + +$ext = $point->extensions?->get(TrackPointExtension::class); +$ext?->hr; // heart rate +``` + +The `Extensions` model is now a keyed collection: +- `set(ExtensionInterface $ext)` — store +- `get(string $class): ?ExtensionInterface` — retrieve by class +- `has(string $class): bool` — check +- `all(): array` — iterate + +**Writing extensions:** +```php +use phpGPX\Models\Extensions; +use phpGPX\Models\Extensions\TrackPointExtension; + +$ext = new TrackPointExtension(); +$ext->hr = 145.0; + +$extensions = new Extensions(); +$extensions->set($ext); +$point->extensions = $extensions; +``` + +**Custom extensions** use a registry: +```php +$gpx = new phpGPX(); +$gpx->registerExtension( + 'http://example.com/ext/v1', + MyExtensionParser::class, + 'myext', // XML prefix for serialization +); +``` + +**Removed:** +- `AbstractExtension` base class — implement `ExtensionInterface` directly +- `GpxSerializable` interface +- Named extension properties on `Extensions` model + +--- + +## 6. PointType Enum + +String constants replaced by a backed enum. + +**Before (1.x):** +```php +$point = new Point(Point::TRACKPOINT); +$type = $point->getPointType(); // 'trkpt' +``` + +**After (2.x):** +```php +use phpGPX\Models\PointType; + +$point = new Point(PointType::Trackpoint); +$type = $point->getPointType(); // PointType::Trackpoint +$tag = $type->value; // 'trkpt' +``` + +Enum cases: `PointType::Waypoint`, `PointType::Trackpoint`, `PointType::Routepoint`. + +--- + +## 7. Constructor Promotion on Value Models + +Simple models now use constructor promotion. Both styles work: + +```php +// Named arguments (new) +$bounds = new Bounds(minLatitude: 48.0, maxLatitude: 49.0); + +// Property assignment (still works) +$bounds = new Bounds(); +$bounds->minLatitude = 48.0; +``` + +Affected models: `Bounds`, `Email`, `Copyright`, `Link`, `Person`. + +--- + +## Quick Reference + +| Removed | Replacement | +|---|---| +| `phpGPX::load()` (static) | `(new phpGPX())->load()` | +| `phpGPX::$PRETTY_PRINT` | `new Config(prettyPrint: true)` | +| `phpGPX::$CALCULATE_STATS` | `engine: Engine::default()` | +| `$model->toArray()` | `$model->jsonSerialize()` | +| `$model->recalculateStats()` | `Engine::default()->process($file)` | +| `$ext->trackPointExtension` | `$ext->get(TrackPointExtension::class)` | +| `Point::TRACKPOINT` | `PointType::Trackpoint` | +| `GpxSerializable` | Removed (parsers handle XML) | +| `Summarizable` | Removed (use `JsonSerializable`) | +| `AbstractExtension` | Implement `ExtensionInterface` | +| `StatsCalculator` | Removed (engine handles stats) | +| `Stats::reset()` | Removed (engine creates new Stats) | \ No newline at end of file diff --git a/docs/04_Development/04_Stats_Architecture.md b/docs/development/stats-architecture.md similarity index 100% rename from docs/04_Development/04_Stats_Architecture.md rename to docs/development/stats-architecture.md diff --git a/docs/04_Development/02_Testing.md b/docs/development/testing.md similarity index 100% rename from docs/04_Development/02_Testing.md rename to docs/development/testing.md diff --git a/docs/00_Getting_Started/01_Installation.md b/docs/getting-started/installation.md similarity index 100% rename from docs/00_Getting_Started/01_Installation.md rename to docs/getting-started/installation.md diff --git a/docs/00_Getting_Started/02_Quick_Start.md b/docs/getting-started/quick-start.md similarity index 100% rename from docs/00_Getting_Started/02_Quick_Start.md rename to docs/getting-started/quick-start.md diff --git a/docs/02_Output_Formats/02_JSON.md b/docs/output-formats/json.md similarity index 100% rename from docs/02_Output_Formats/02_JSON.md rename to docs/output-formats/json.md diff --git a/docs/02_Output_Formats/01_XML.md b/docs/output-formats/xml.md similarity index 100% rename from docs/02_Output_Formats/01_XML.md rename to docs/output-formats/xml.md diff --git a/docs/01_Usage/04_Configuration.md b/docs/usage/configuration.md similarity index 98% rename from docs/01_Usage/04_Configuration.md rename to docs/usage/configuration.md index ed290fb..34ed5f1 100644 --- a/docs/01_Usage/04_Configuration.md +++ b/docs/usage/configuration.md @@ -134,4 +134,4 @@ $rawFile = $raw->load('track.gpx'); - Configuration is immutable after construction — `Config` properties are set once via constructor. - JSON output always uses ISO 8601 UTC for datetime values (GeoJSON convention). - Stats are produced exclusively by `Engine` and its analyzers — models are pure data containers. -- Extension registry is configured per-instance. See [Extensions](05_Extensions.md) for custom extension setup. \ No newline at end of file +- Extension registry is configured per-instance. See [Extensions](extensions.md) for custom extension setup. \ No newline at end of file diff --git a/docs/01_Usage/02_Creating_Files.md b/docs/usage/creating-files.md similarity index 100% rename from docs/01_Usage/02_Creating_Files.md rename to docs/usage/creating-files.md diff --git a/docs/01_Usage/05_Extensions.md b/docs/usage/extensions.md similarity index 97% rename from docs/01_Usage/05_Extensions.md rename to docs/usage/extensions.md index 1e4a812..f9ea8a6 100644 --- a/docs/01_Usage/05_Extensions.md +++ b/docs/usage/extensions.md @@ -189,6 +189,6 @@ The `TrackPointExtensionAnalyzer` aggregates sensor data from `TrackPointExtensi - `averageCadence` - `averageTemperature` -These are computed per-segment and aggregated to track level (weighted by point count). See [Statistics](03_Statistics.md) for details. +These are computed per-segment and aggregated to track level (weighted by point count). See [Statistics](statistics.md) for details. -Custom extension analyzers can be built as `PointAnalyzerInterface` implementations and registered with the engine. See [Stats Architecture](../04_Development/04_Stats_Architecture.md). \ No newline at end of file +Custom extension analyzers can be built as `PointAnalyzerInterface` implementations and registered with the engine. See [Stats Architecture](../development/stats-architecture.md). \ No newline at end of file diff --git a/docs/01_Usage/01_Loading_Files.md b/docs/usage/loading-files.md similarity index 98% rename from docs/01_Usage/01_Loading_Files.md rename to docs/usage/loading-files.md index 2735640..54db9bc 100644 --- a/docs/01_Usage/01_Loading_Files.md +++ b/docs/usage/loading-files.md @@ -66,7 +66,7 @@ When loading a GPX file, phpGPX processes: - **Waypoints** (``) - individual points with coordinates, elevation, time, and all optional GPX 1.1 attributes - **Tracks** (``) - containing segments (``) of track points (``) - **Routes** (``) - containing route points (``) -- **Extensions** - Parsed via the extension registry. Garmin TrackPointExtension (heart rate, temperature, cadence) is built-in. Unsupported extensions are preserved as key-value pairs. See [Extensions](05_Extensions.md). +- **Extensions** - Parsed via the extension registry. Garmin TrackPointExtension (heart rate, temperature, cadence) is built-in. Unsupported extensions are preserved as key-value pairs. See [Extensions](extensions.md). ## Processing pipeline diff --git a/docs/01_Usage/03_Statistics.md b/docs/usage/statistics.md similarity index 100% rename from docs/01_Usage/03_Statistics.md rename to docs/usage/statistics.md diff --git a/examples/Example.php b/examples/Example.php index c08b5f0..56abd4f 100644 --- a/examples/Example.php +++ b/examples/Example.php @@ -5,15 +5,25 @@ */ use phpGPX\phpGPX; +use phpGPX\Config; +use phpGPX\Analysis\Engine; require_once '../vendor/autoload.php'; -$gpx = new phpGPX(); -$file = $gpx->load('endomondo.gpx'); +$gpx = new phpGPX( + config: new Config(prettyPrint: true), + engine: Engine::default(), +); -phpGPX::$PRETTY_PRINT = true; -//$file->save('output_Evening_Ride.gpx', phpGPX::XML_FORMAT); +$file = $gpx->load('endomondo.gpx'); foreach ($file->tracks as $track) { - var_dump($track->stats->toArray()); -} + echo "Track: " . $track->name . "\n"; + echo "Distance: " . round($track->stats->distance) . " m\n"; + echo "Duration: " . $track->stats->duration . " s\n"; + echo "Avg speed: " . round($track->stats->averageSpeed, 2) . " m/s\n"; + echo "Elevation gain: " . round($track->stats->cumulativeElevationGain, 1) . " m\n"; + echo "Elevation loss: " . round($track->stats->cumulativeElevationLoss, 1) . " m\n"; + echo "\nFull stats:\n"; + var_dump($track->stats->jsonSerialize()); +} \ No newline at end of file diff --git a/examples/waypoints_load.php b/examples/waypoints_load.php index b9574d9..02dc4ee 100644 --- a/examples/waypoints_load.php +++ b/examples/waypoints_load.php @@ -5,25 +5,23 @@ */ use phpGPX\phpGPX; +use phpGPX\Config; require_once '../vendor/autoload.php'; $origFile = dirname(__FILE__).'/waypoint_test.gpx'; $outFile = dirname(__FILE__).'/output_waypoint_test.gpx'; -// $outFile2 = dirname(__FILE__).'/output_waypoint_test2.gpx'; -$gpx = new phpGPX(); +$gpx = new phpGPX(config: new Config(prettyPrint: true)); $file = $gpx->load($origFile); -phpGPX::$PRETTY_PRINT = true; $file->save($outFile, phpGPX::XML_FORMAT); $retcode = 0; system("diff $origFile $outFile", $retcode); -// system("diff $origFile $outFile2", $retcode); if ($retcode != 0) { - throw new \Exception("wapoint file incorrect"); + throw new \Exception("waypoint file incorrect"); } else { - print "wapoint test successfull\n"; -} + print "waypoint test successful\n"; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8d08be6 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,66 @@ +site_name: phpGPX +site_description: A PHP library for reading, creating, and manipulating GPX files +site_url: https://sibyx.github.io/phpGPX/develop/ +repo_url: https://github.com/Sibyx/phpGPX +repo_name: Sibyx/phpGPX + +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: teal + accent: teal + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: teal + accent: teal + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.sections + - navigation.expand + - navigation.top + - content.code.copy + - content.code.annotate + - search.highlight + icon: + repo: fontawesome/brands/github + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true + - tables + - toc: + permalink: true + - attr_list + +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started/installation.md + - Quick Start: getting-started/quick-start.md + - Usage: + - Loading Files: usage/loading-files.md + - Creating Files: usage/creating-files.md + - Statistics: usage/statistics.md + - Configuration: usage/configuration.md + - Extensions: usage/extensions.md + - Output Formats: + - XML (GPX): output-formats/xml.md + - JSON (GeoJSON): output-formats/json.md + - Development: + - Contributing: development/contributing.md + - Testing: development/testing.md + - Stats Architecture: development/stats-architecture.md + - Migration Guide (1.x → 2.x): development/migration.md \ No newline at end of file diff --git a/phpdoc.xml b/phpdoc.xml index 5b750e1..065f870 100644 --- a/phpdoc.xml +++ b/phpdoc.xml @@ -22,11 +22,5 @@ phpGPX - - - docs - - guide - \ No newline at end of file From e0e790b052b80dc4db4a827063d997ea59caeed8 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 03:34:16 +0100 Subject: [PATCH 26/31] =?UTF-8?q?Update=20docs=20workflow=20to=20ensure=20?= =?UTF-8?q?versioning=20and=20add=20version=20index=20page=20=F0=9F=99=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 99 +++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c1327ad..1f4e312 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,7 @@ on: paths: - 'docs/**' - 'mkdocs.yml' + - '.github/workflows/docs.yml' workflow_dispatch: permissions: @@ -49,6 +50,100 @@ jobs: rm -rf develop/ cp -r site/ develop/ + # Ensure v1 docs exist (copy from master if missing) + if [ ! -d "v1" ]; then + git checkout origin/master -- docs/ 2>/dev/null || true + if [ -d "docs" ]; then + mv docs v1 + fi + fi + + # Generate version index page + cat > index.html << 'INDEXEOF' + + + + + + phpGPX Documentation + + + +
+

phpGPX

+

A PHP library for reading, creating, and manipulating GPX files

+ +

+ GitHub · + Packagist +

+
+ + + INDEXEOF + # Commit and push - git add develop/ - git diff --cached --quiet || (git commit -m "Deploy docs (develop) from ${GITHUB_SHA::8}" && git push origin gh-pages) \ No newline at end of file + git add develop/ index.html + [ -d "v1" ] && git add v1/ + git diff --cached --quiet || (git commit -m "Deploy docs from ${GITHUB_SHA::8}" && git push origin gh-pages) \ No newline at end of file From 4b29361acb0f2a7c91af064bf7652a6b788071a1 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 03:41:33 +0100 Subject: [PATCH 27/31] =?UTF-8?q?Update=20docs=20workflow=20and=20mkdocs?= =?UTF-8?q?=20config:=20add=20PlantUML=20support,=20edit/view=20actions,?= =?UTF-8?q?=20and=20enable=20advanced=20navigation=20features=20?= =?UTF-8?q?=F0=9F=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- mkdocs.yml | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1f4e312..9d6ce21 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: python-version: '3.12' - name: Install mkdocs-material - run: pip install mkdocs-material + run: pip install mkdocs-material plantuml-markdown - name: Build and deploy to gh-pages (develop/) run: | diff --git a/mkdocs.yml b/mkdocs.yml index 8d08be6..b2a3e00 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ site_description: A PHP library for reading, creating, and manipulating GPX file site_url: https://sibyx.github.io/phpGPX/develop/ repo_url: https://github.com/Sibyx/phpGPX repo_name: Sibyx/phpGPX +edit_uri: edit/develop/docs/ theme: name: material @@ -22,14 +23,26 @@ theme: icon: material/brightness-4 name: Switch to light mode features: + - navigation.instant + - navigation.instant.progress + - navigation.tracking - navigation.sections - navigation.expand - navigation.top + - navigation.indexes + - navigation.path - content.code.copy - content.code.annotate + - content.action.edit + - content.action.view + - search.suggest - search.highlight + - search.share + - toc.follow icon: repo: fontawesome/brands/github + edit: material/pencil + view: material/eye markdown_extensions: - admonition @@ -37,13 +50,23 @@ markdown_extensions: - pymdownx.superfences - pymdownx.highlight: anchor_linenums: true + line_spans: __span + pygments_lang_class: true + auto_title: true - pymdownx.inlinehilite + - pymdownx.snippets - pymdownx.tabbed: alternate_style: true + - pymdownx.mark + - plantuml_markdown: + server: https://www.plantuml.com/plantuml + format: svg - tables - toc: permalink: true - attr_list + - md_in_html + - def_list nav: - Home: index.md From ace167c8f2b0ec9fc672b9f6f3abf3188871f9a1 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 10:50:11 +0100 Subject: [PATCH 28/31] =?UTF-8?q?Polishing=20README.md=20=F0=9F=8D=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 189 ++++++++++++++++++++++++------------------------------ 1 file changed, 85 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index a4b99f7..9a6aee6 100644 --- a/README.md +++ b/README.md @@ -4,58 +4,22 @@ [![Latest development](https://img.shields.io/packagist/vpre/sibyx/phpgpx.svg)](https://packagist.org/packages/sibyx/phpgpx) [![Packagist downloads](https://img.shields.io/packagist/dm/sibyx/phpgpx.svg)](https://packagist.org/packages/sibyx/phpgpx) +PHP library for reading, creating, and manipulating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format). -Simple library written in PHP for reading and creating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format). - -Contribution and feedback is welcome! Please check the issues for TODO. I will be happy every feature or pull request. - -Repository branches: - -- `master`: latest stable version -- `develop`: works on `2.x` - -## Features - - - Full support of [official specification](http://www.topografix.com/GPX/1/1/). - - Single-pass stats engine with pluggable analyzers. - - Extensions support. - - JSON (GeoJSON) & XML output. - -### Extension Registry - -Built-in support for Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd) (v1 + v2). -Custom extensions can be registered via `ExtensionInterface` + `ExtensionParserInterface`: - -```php -$gpx = new phpGPX(); -$gpx->registerExtension('http://example.com/ext/v1', MyExtensionParser::class, 'myext'); -``` - -### Stats calculation - -Stats are provided by the `Engine` and its analyzers: - -- (Smoothed) Distance (m) — `DistanceAnalyzer` -- Average speed (m/s), average pace (s/km) — derived by engine -- Min / max altitude with coordinates — `AltitudeAnalyzer` -- (Smoothed) Elevation gain / loss (m) — `ElevationAnalyzer` -- Start / end timestamps with coordinates — `TimestampAnalyzer` -- Duration (seconds) — derived by engine -- Coordinate bounds (min/max lat/lon) — `BoundsAnalyzer` -- Moving duration and moving average speed — `MovementAnalyzer` -- Heart rate, cadence, temperature — `TrackPointExtensionAnalyzer` +- Full [GPX 1.1 specification](http://www.topografix.com/GPX/1/1/) support +- Single-pass stats engine with pluggable analyzers +- Extension registry (Garmin TrackPointExtension built-in) +- XML and GeoJSON (RFC 7946) output ## Installation -You can easily install phpGPX library with [composer](https://getcomposer.org/). - ``` composer require sibyx/phpgpx ``` -## Examples +Requires PHP >= 8.1. -### Open GPX file and load basic stats +## Quick Start ```php load('example.gpx'); foreach ($file->tracks as $track) { - // Statistics for whole track echo "Distance: " . round($track->stats->distance) . " m\n"; echo "Duration: " . gmdate("H:i:s", $track->stats->duration) . "\n"; foreach ($track->segments as $segment) { - // Statistics for segment of track echo " Segment distance: " . round($segment->stats->distance) . " m\n"; } } ``` -### Writing to file +### Saving files + +```php +$file->save('output.gpx', phpGPX::XML_FORMAT); +$file->save('output.json', phpGPX::JSON_FORMAT); +``` + +## Advanced Usage + +### Configuration + +Output formatting is configured via the `Config` value object. Stats computation is configured via analyzer constructor arguments. + ```php -load('example.gpx'); +$file = $gpx->load('track.gpx'); +``` -// XML -$file->save('output.gpx', phpGPX::XML_FORMAT); +### Custom engine -// JSON (GeoJSON) -$file->save('output.json', phpGPX::JSON_FORMAT); +For fine-grained control, build the engine manually with only the analyzers you need: + +```php +use phpGPX\Analysis\Engine; +use phpGPX\Analysis\DistanceAnalyzer; +use phpGPX\Analysis\ElevationAnalyzer; +use phpGPX\Analysis\AltitudeAnalyzer; +use phpGPX\Analysis\TimestampAnalyzer; +use phpGPX\Analysis\BoundsAnalyzer; + +$engine = (new Engine()) + ->addAnalyzer(new DistanceAnalyzer(applySmoothing: true, smoothingThreshold: 3)) + ->addAnalyzer(new ElevationAnalyzer(applySmoothing: true, spikesThreshold: 100)) + ->addAnalyzer(new AltitudeAnalyzer()) + ->addAnalyzer(new TimestampAnalyzer()) + ->addAnalyzer(new BoundsAnalyzer()); + +$gpx->setEngine($engine); ``` -### Creating file from scratch +### Stats reference + +The engine provides the following stats through its analyzers: + +| Stat | Analyzer | +|-----------------------------------------|-------------------------------| +| Distance (m), smoothed | `DistanceAnalyzer` | +| Average speed (m/s), pace (s/km) | derived by engine | +| Min / max altitude with coordinates | `AltitudeAnalyzer` | +| Elevation gain / loss (m), smoothed | `ElevationAnalyzer` | +| Start / end timestamps with coordinates | `TimestampAnalyzer` | +| Duration (seconds) | derived by engine | +| Coordinate bounds (min/max lat/lon) | `BoundsAnalyzer` | +| Moving duration, moving avg speed | `MovementAnalyzer` | +| Heart rate, cadence, temperature | `TrackPointExtensionAnalyzer` | + +### Custom extensions + +Built-in support for Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd) (v1 + v2). Register your own via `ExtensionInterface` + `ExtensionParserInterface`: + ```php -registerExtension('http://example.com/ext/v1', MyExtensionParser::class, 'myext'); +``` + +### Creating a file from scratch +```php +points[] = $point; $track->segments[] = $segment; $gpx_file->tracks[] = $track; -// Save as GPX XML $gpx_file->save('output.gpx', \phpGPX\phpGPX::XML_FORMAT); - -// Save as GeoJSON $gpx_file->save('output.json', \phpGPX\phpGPX::JSON_FORMAT); ``` -Currently supported output formats: - - - XML - - JSON (GeoJSON, RFC 7946) - -## Configuration +## Contributing -Output formatting is configured via the `Config` value object. Stats computation is configured via analyzer constructor arguments. - -```php -use phpGPX\phpGPX; -use phpGPX\Config; -use phpGPX\Analysis\Engine; +Contributions and feedback are welcome! Please check [the issues](https://github.com/Sibyx/phpGPX/issues). -$gpx = new phpGPX( - config: new Config(prettyPrint: true), - engine: Engine::default( - sortByTimestamp: true, - applyElevationSmoothing: true, - elevationSmoothingThreshold: 2, - ignoreZeroElevation: false, - ), -); - -$file = $gpx->load('track.gpx'); -``` - -For fine-grained control, build the engine manually: - -```php -use phpGPX\Analysis\Engine; -use phpGPX\Analysis\DistanceAnalyzer; -use phpGPX\Analysis\ElevationAnalyzer; -use phpGPX\Analysis\AltitudeAnalyzer; -use phpGPX\Analysis\TimestampAnalyzer; -use phpGPX\Analysis\BoundsAnalyzer; - -$engine = (new Engine()) - ->addAnalyzer(new DistanceAnalyzer(applySmoothing: true, smoothingThreshold: 3)) - ->addAnalyzer(new ElevationAnalyzer(applySmoothing: true, spikesThreshold: 100)) - ->addAnalyzer(new AltitudeAnalyzer()) - ->addAnalyzer(new TimestampAnalyzer()) - ->addAnalyzer(new BoundsAnalyzer()); - -$gpx->setEngine($engine); -``` +Repository branches: +- `master` — latest stable release +- `develop` — 2.x development -This library started as part of my job at [BACKBONE, s.r.o.](https://www.backbone.sk/en/). -Thank you very much for their support! +This library started as part of my job at [BACKBONE, s.r.o.](https://www.backbone.sk/en/). Thank you for their support! ## License From cd120b3726767ed737c1291fed88390fb4488d99 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 11:16:51 +0100 Subject: [PATCH 29/31] =?UTF-8?q?Cleaning=20up=20before=20`2.0.0-beta.1`?= =?UTF-8?q?=20=F0=9F=98=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 +- .github/workflows/docs.yml | 4 +- .gitignore | 5 +- .php-cs-fixer.php | 37 +- CHANGELOG.md | 48 +- Dockerfile | 22 - composer.json | 2 +- docker-compose.yml | 7 - docs/development/contributing.md | 22 +- docs/rfc7946.txt | 1571 ------------------------------ phpdoc.xml | 26 - src/phpGPX/phpGPX.php | 2 +- 12 files changed, 82 insertions(+), 1668 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml delete mode 100644 docs/rfc7946.txt delete mode 100644 phpdoc.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32f397e..8595a71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,9 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1', '8.2', '8.3', '8.4'] + php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9d6ce21..2aab6c9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,11 +16,11 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.gitignore b/.gitignore index d5dbd6c..185d5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ composer.lock /.phpunit.result.cache /.phpunit.cache coverage.xml -/docs-output/ -/.phpdoc/ /site/ -/.venv/ \ No newline at end of file +/.venv/ +/.php-cs-fixer.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 6c21654..3d9b619 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,20 +1,25 @@ - in(__DIR__) - ->ignoreDotFiles(true) - ->ignoreVCS(true) - ->exclude(['docs', 'vendor']) - ->files() - ->name('*.php') +$finder = (new PhpCsFixer\Finder()) + ->in(__DIR__) + ->ignoreDotFiles(true) + ->ignoreVCS(true) + ->exclude(['docs', 'vendor']) + ->name('*.php') ; -return PhpCsFixer\Config::create() - ->setUsingCache(true) - ->setFinder($finder) - ->setRules([ - '@PSR2' => true, - ]) - ->setIndent("\t") -; +return (new PhpCsFixer\Config()) + ->setUsingCache(true) + ->setFinder($finder) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'no_unused_imports' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'single_quote' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arguments', 'arrays', 'parameters']], + 'no_whitespace_in_blank_line' => true, + 'no_trailing_whitespace' => true, + ]) + ->setIndent("\t") +; \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index be4cd14..77aaeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,39 @@ # Changelog -## 2.0.0 : TBD - -- **Changed** Support for PHP 8.1+ only -- **Fixed** Added proper return type declarations to parser classes to fix deprecated messages in PHP 8.4 -- **Fixed** Patched vendor files to fix deprecation warnings about implicitly marking parameters as nullable: - - sebastian/cli-parser/src/Parser.php - - phpunit/phpunit/src/Util/Exporter.php - - sebastian/exporter/src/Exporter.php -- **Fixed** Updated tests to be compatible with PHPUnit 12.x: - - Added missing tests to BoundsTest class - - Updated test annotations to use PHPUnit 12 attributes - - Fixed data provider usage in SerializationHelperTest - - Implemented missing GpxSerializable interface methods in Bounds class - - Updated phpunit.xml configuration +## 2.0.0-beta.1 : 2025-03-09 + +### Breaking Changes + +- **Changed**: `phpGPX` is now instance-based — `phpGPX::load()` (static) replaced by `(new phpGPX())->load()` +- **Removed**: All static configuration properties (`$CALCULATE_STATS`, `$SORT_BY_TIMESTAMP`, `$PRETTY_PRINT`, etc.) — replaced by `Config` value object and analyzer constructors +- **Removed**: `Summarizable` interface and `toArray()` — replaced by `JsonSerializable` returning GeoJSON (RFC 7946) +- **Removed**: `GpxSerializable` interface — parsers handle XML serialization via Data Mapper pattern +- **Removed**: `StatsCalculator` interface — replaced by Engine +- **Removed**: `AbstractExtension` base class — replaced by `ExtensionInterface` +- **Changed**: Point type constants (`Point::TRACKPOINT`, etc.) replaced by `PointType` enum +- **Changed**: Extension access `$extensions->trackPointExtension` replaced by `$extensions->get(TrackPointExtension::class)` + +### Added + +- **Added**: Single-pass stats `Engine` with pluggable analyzers (`DistanceAnalyzer`, `ElevationAnalyzer`, `AltitudeAnalyzer`, `TimestampAnalyzer`, `BoundsAnalyzer`, `MovementAnalyzer`, `TrackPointExtensionAnalyzer`) +- **Added**: `Engine::default()` factory with named parameters for common configuration +- **Added**: `ExtensionRegistry` for registering custom extension parsers by namespace URI +- **Added**: `ExtensionInterface` and `ExtensionParserInterface` for custom extensions +- **Added**: `Config` value object for output configuration +- **Added**: `AbstractParser` base class centralizing attribute mapping and XML handling +- **Added**: mkdocs-material documentation site with PlantUML support +- **Added**: Consolidated CI workflow (PHP 8.1–8.4 matrix) with Codecov integration + +### Changed + +- **Changed**: PHP 8.1+ required +- **Changed**: `Extensions` model is now a keyed collection +- **Changed**: Default Garmin TrackPointExtension v1 + v2 auto-registered via `ExtensionRegistry::default()` +- **Changed**: All parsers refactored to extend `AbstractParser` +- **Changed**: Strict typing on all model properties +- **Changed**: Test suite restructured into `unit` and `integration` suites +- **Changed**: PHPUnit 10+ with attributes (annotations removed) +- **Changed**: Test fixtures standardized under `tests/Fixtures/` ## 1.3.0 : 2023-07-19 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1b7ddd8..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM php:8.4-cli - -RUN apt-get update && apt-get install -y \ - libxml2-dev \ - git \ - unzip \ - curl \ - && docker-php-ext-install dom simplexml \ - && pecl install xdebug \ - && docker-php-ext-enable xdebug \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -RUN echo "xdebug.mode=debug,coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ - && echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ - && echo "xdebug.start_with_request=trigger" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -RUN curl -sL https://phpdoc.org/phpDocumentor.phar -o /usr/local/bin/phpdoc \ - && chmod +x /usr/local/bin/phpdoc - -WORKDIR /app \ No newline at end of file diff --git a/composer.json b/composer.json index 57e9c5b..e23ae06 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "sibyx/phpgpx", "type": "library", - "version": "1.3.0", + "version": "2.0.0-beta.1", "description": "A simple PHP library for GPX manipulation", "minimum-stability": "stable", "keywords": [ diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 621a434..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - php: - build: . - volumes: - - .:/app - environment: - - XDEBUG_MODE=debug,coverage \ No newline at end of file diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 7cd5d3d..a5feb48 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -9,8 +9,8 @@ - `tests/` - Test suite - `Unit/` - Unit tests for individual components - `Integration/` - Full file load/save round-trip tests - - `fixtures/` - GPX test fixture files -- `docs/` - Documentation (Daux.io) + - `Fixtures/` - GPX and parser test fixture files +- `docs/` - Documentation (mkdocs-material) ## Branches @@ -27,4 +27,20 @@ composer install ## Code style -The project uses PSR-2 with **tab indentation** (configured in `.php-cs-fixer.php`). \ No newline at end of file +The project follows **PSR-12** with **tab indentation**, enforced by [PHP CS Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) (configured in `.php-cs-fixer.php`). + +```bash +# Check for style violations (dry run) +composer cs-fix -- --dry-run + +# Auto-fix all files +composer cs-fix +``` + +Key rules beyond PSR-12: + +- Short array syntax (`[]` not `array()`) +- No unused imports +- Alphabetically ordered imports +- Single quotes for strings +- Trailing commas in multiline arguments, arrays, and parameters \ No newline at end of file diff --git a/docs/rfc7946.txt b/docs/rfc7946.txt deleted file mode 100644 index 109adb8..0000000 --- a/docs/rfc7946.txt +++ /dev/null @@ -1,1571 +0,0 @@ - - - - - - -Internet Engineering Task Force (IETF) H. Butler -Request for Comments: 7946 Hobu Inc. -Category: Standards Track M. Daly -ISSN: 2070-1721 Cadcorp - A. Doyle - - S. Gillies - Mapbox - S. Hagen - - T. Schaub - Planet Labs - August 2016 - - - The GeoJSON Format - -Abstract - - GeoJSON is a geospatial data interchange format based on JavaScript - Object Notation (JSON). It defines several types of JSON objects and - the manner in which they are combined to represent data about - geographic features, their properties, and their spatial extents. - GeoJSON uses a geographic coordinate reference system, World Geodetic - System 1984, and units of decimal degrees. - -Status of This Memo - - This is an Internet Standards Track document. - - This document is a product of the Internet Engineering Task Force - (IETF). It represents the consensus of the IETF community. It has - received public review and has been approved for publication by the - Internet Engineering Steering Group (IESG). Further information on - Internet Standards is available in Section 2 of RFC 7841. - - Information about the current status of this document, any errata, - and how to provide feedback on it may be obtained at - http://www.rfc-editor.org/info/rfc7946. - - - - - - - - - - - - -Butler, et al. Standards Track [Page 1] - -RFC 7946 GeoJSON August 2016 - - -Copyright Notice - - Copyright (c) 2016 IETF Trust and the persons identified as the - document authors. All rights reserved. - - This document is subject to BCP 78 and the IETF Trust's Legal - Provisions Relating to IETF Documents - (http://trustee.ietf.org/license-info) in effect on the date of - publication of this document. Please review these documents - carefully, as they describe your rights and restrictions with respect - to this document. Code Components extracted from this document must - include Simplified BSD License text as described in Section 4.e of - the Trust Legal Provisions and are provided without warranty as - described in the Simplified BSD License. - -Table of Contents - - 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 - 1.1. Requirements Language . . . . . . . . . . . . . . . . . . 4 - 1.2. Conventions Used in This Document . . . . . . . . . . . . 4 - 1.3. Specification of GeoJSON . . . . . . . . . . . . . . . . 4 - 1.4. Definitions . . . . . . . . . . . . . . . . . . . . . . . 5 - 1.5. Example . . . . . . . . . . . . . . . . . . . . . . . . . 5 - 2. GeoJSON Text . . . . . . . . . . . . . . . . . . . . . . . . 6 - 3. GeoJSON Object . . . . . . . . . . . . . . . . . . . . . . . 6 - 3.1. Geometry Object . . . . . . . . . . . . . . . . . . . . . 7 - 3.1.1. Position . . . . . . . . . . . . . . . . . . . . . . 7 - 3.1.2. Point . . . . . . . . . . . . . . . . . . . . . . . . 8 - 3.1.3. MultiPoint . . . . . . . . . . . . . . . . . . . . . 8 - 3.1.4. LineString . . . . . . . . . . . . . . . . . . . . . 8 - 3.1.5. MultiLineString . . . . . . . . . . . . . . . . . . . 8 - 3.1.6. Polygon . . . . . . . . . . . . . . . . . . . . . . . 9 - 3.1.7. MultiPolygon . . . . . . . . . . . . . . . . . . . . 9 - 3.1.8. GeometryCollection . . . . . . . . . . . . . . . . . 9 - 3.1.9. Antimeridian Cutting . . . . . . . . . . . . . . . . 10 - 3.1.10. Uncertainty and Precision . . . . . . . . . . . . . . 11 - 3.2. Feature Object . . . . . . . . . . . . . . . . . . . . . 11 - 3.3. FeatureCollection Object . . . . . . . . . . . . . . . . 12 - 4. Coordinate Reference System . . . . . . . . . . . . . . . . . 12 - 5. Bounding Box . . . . . . . . . . . . . . . . . . . . . . . . 12 - 5.1. The Connecting Lines . . . . . . . . . . . . . . . . . . 14 - 5.2. The Antimeridian . . . . . . . . . . . . . . . . . . . . 14 - 5.3. The Poles . . . . . . . . . . . . . . . . . . . . . . . . 14 - 6. Extending GeoJSON . . . . . . . . . . . . . . . . . . . . . . 15 - 6.1. Foreign Members . . . . . . . . . . . . . . . . . . . . . 15 - 7. GeoJSON Types Are Not Extensible . . . . . . . . . . . . . . 16 - 7.1. Semantics of GeoJSON Members and Types Are Not Changeable 16 - 8. Versioning . . . . . . . . . . . . . . . . . . . . . . . . . 17 - - - -Butler, et al. Standards Track [Page 2] - -RFC 7946 GeoJSON August 2016 - - - 9. Mapping 'geo' URIs . . . . . . . . . . . . . . . . . . . . . 17 - 10. Security Considerations . . . . . . . . . . . . . . . . . . . 18 - 11. Interoperability Considerations . . . . . . . . . . . . . . . 18 - 11.1. I-JSON . . . . . . . . . . . . . . . . . . . . . . . . . 18 - 11.2. Coordinate Precision . . . . . . . . . . . . . . . . . . 18 - 12. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 19 - 13. References . . . . . . . . . . . . . . . . . . . . . . . . . 20 - 13.1. Normative References . . . . . . . . . . . . . . . . . . 20 - 13.2. Informative References . . . . . . . . . . . . . . . . . 21 - Appendix A. Geometry Examples . . . . . . . . . . . . . . . . . 22 - A.1. Points . . . . . . . . . . . . . . . . . . . . . . . . . 22 - A.2. LineStrings . . . . . . . . . . . . . . . . . . . . . . . 22 - A.3. Polygons . . . . . . . . . . . . . . . . . . . . . . . . 23 - A.4. MultiPoints . . . . . . . . . . . . . . . . . . . . . . . 24 - A.5. MultiLineStrings . . . . . . . . . . . . . . . . . . . . 24 - A.6. MultiPolygons . . . . . . . . . . . . . . . . . . . . . . 25 - A.7. GeometryCollections . . . . . . . . . . . . . . . . . . . 26 - Appendix B. Changes from the Pre-IETF GeoJSON Format - Specification . . . . . . . . . . . . . . . . . . . 26 - B.1. Normative Changes . . . . . . . . . . . . . . . . . . . . 26 - B.2. Informative Changes . . . . . . . . . . . . . . . . . . . 27 - Appendix C. GeoJSON Text Sequences . . . . . . . . . . . . . . . 27 - Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 27 - Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 28 - -1. Introduction - - GeoJSON is a format for encoding a variety of geographic data - structures using JavaScript Object Notation (JSON) [RFC7159]. A - GeoJSON object may represent a region of space (a Geometry), a - spatially bounded entity (a Feature), or a list of Features (a - FeatureCollection). GeoJSON supports the following geometry types: - Point, LineString, Polygon, MultiPoint, MultiLineString, - MultiPolygon, and GeometryCollection. Features in GeoJSON contain a - Geometry object and additional properties, and a FeatureCollection - contains a list of Features. - - The format is concerned with geographic data in the broadest sense; - anything with qualities that are bounded in geographical space might - be a Feature whether or not it is a physical structure. The concepts - in GeoJSON are not new; they are derived from preexisting open - geographic information system standards and have been streamlined to - better suit web application development using JSON. - - GeoJSON comprises the seven concrete geometry types defined in the - OpenGIS Simple Features Implementation Specification for SQL [SFSQL]: - 0-dimensional Point and MultiPoint; 1-dimensional curve LineString - and MultiLineString; 2-dimensional surface Polygon and MultiPolygon; - - - -Butler, et al. Standards Track [Page 3] - -RFC 7946 GeoJSON August 2016 - - - and the heterogeneous GeometryCollection. GeoJSON representations of - instances of these geometry types are analogous to the well-known - binary (WKB) and well-known text (WKT) representations described in - that same specification. - - GeoJSON also comprises the types Feature and FeatureCollection. - Feature objects in GeoJSON contain a Geometry object with one of the - above geometry types and additional members. A FeatureCollection - object contains an array of Feature objects. This structure is - analogous to that of the Web Feature Service (WFS) response to - GetFeatures requests specified in [WFSv1] or to a Keyhole Markup - Language (KML) Folder of Placemarks [KMLv2.2]. Some implementations - of the WFS specification also provide GeoJSON-formatted responses to - GetFeature requests, but there is no particular service model or - Feature type ontology implied in the GeoJSON format specification. - - Since its initial publication in 2008 [GJ2008], the GeoJSON format - specification has steadily grown in popularity. It is widely used in - JavaScript web-mapping libraries, JSON-based document databases, and - web APIs. - -1.1. Requirements Language - - The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", - "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and - "OPTIONAL" in this document are to be interpreted as described in - [RFC2119]. - -1.2. Conventions Used in This Document - - The ordering of the members of any JSON object defined in this - document MUST be considered irrelevant, as specified by [RFC7159]. - - Some examples use the combination of a JavaScript single-line comment - (//) followed by an ellipsis (...) as placeholder notation for - content deemed irrelevant by the authors. These placeholders must of - course be deleted or otherwise replaced, before attempting to - validate the corresponding JSON code example. - - Whitespace is used in the examples inside this document to help - illustrate the data structures, but it is not required. Unquoted - whitespace is not significant in JSON. - -1.3. Specification of GeoJSON - - This document supersedes the original GeoJSON format specification - [GJ2008]. - - - - -Butler, et al. Standards Track [Page 4] - -RFC 7946 GeoJSON August 2016 - - -1.4. Definitions - - o JavaScript Object Notation (JSON), and the terms object, member, - name, value, array, number, true, false, and null, are to be - interpreted as defined in [RFC7159]. - - o Inside this document, the term "geometry type" refers to seven - case-sensitive strings: "Point", "MultiPoint", "LineString", - "MultiLineString", "Polygon", "MultiPolygon", and - "GeometryCollection". - - o As another shorthand notation, the term "GeoJSON types" refers to - nine case-sensitive strings: "Feature", "FeatureCollection", and - the geometry types listed above. - - o The word "Collection" in "FeatureCollection" and - "GeometryCollection" does not have any significance for the - semantics of array members. The "features" and "geometries" - members, respectively, of these objects are standard ordered JSON - arrays, not unordered sets. - -1.5. Example - - A GeoJSON FeatureCollection: - - { - "type": "FeatureCollection", - "features": [{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [102.0, 0.5] - }, - "properties": { - "prop0": "value0" - } - }, { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [102.0, 0.0], - [103.0, 1.0], - [104.0, 0.0], - [105.0, 1.0] - ] - }, - "properties": { - - - -Butler, et al. Standards Track [Page 5] - -RFC 7946 GeoJSON August 2016 - - - "prop0": "value0", - "prop1": 0.0 - } - }, { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0] - ] - ] - }, - "properties": { - "prop0": "value0", - "prop1": { - "this": "that" - } - } - }] - } - -2. GeoJSON Text - - A GeoJSON text is a JSON text and consists of a single GeoJSON - object. - -3. GeoJSON Object - - A GeoJSON object represents a Geometry, Feature, or collection of - Features. - - o A GeoJSON object is a JSON object. - - o A GeoJSON object has a member with the name "type". The value of - the member MUST be one of the GeoJSON types. - - o A GeoJSON object MAY have a "bbox" member, the value of which MUST - be a bounding box array (see Section 5). - - o A GeoJSON object MAY have other members (see Section 6). - - - - - - -Butler, et al. Standards Track [Page 6] - -RFC 7946 GeoJSON August 2016 - - -3.1. Geometry Object - - A Geometry object represents points, curves, and surfaces in - coordinate space. Every Geometry object is a GeoJSON object no - matter where it occurs in a GeoJSON text. - - o The value of a Geometry object's "type" member MUST be one of the - seven geometry types (see Section 1.4). - - o A GeoJSON Geometry object of any type other than - "GeometryCollection" has a member with the name "coordinates". - The value of the "coordinates" member is an array. The structure - of the elements in this array is determined by the type of - geometry. GeoJSON processors MAY interpret Geometry objects with - empty "coordinates" arrays as null objects. - -3.1.1. Position - - A position is the fundamental geometry construct. The "coordinates" - member of a Geometry object is composed of either: - - o one position in the case of a Point geometry, - - o an array of positions in the case of a LineString or MultiPoint - geometry, - - o an array of LineString or linear ring (see Section 3.1.6) - coordinates in the case of a Polygon or MultiLineString geometry, - or - - o an array of Polygon coordinates in the case of a MultiPolygon - geometry. - - A position is an array of numbers. There MUST be two or more - elements. The first two elements are longitude and latitude, or - easting and northing, precisely in that order and using decimal - numbers. Altitude or elevation MAY be included as an optional third - element. - - Implementations SHOULD NOT extend positions beyond three elements - because the semantics of extra elements are unspecified and - ambiguous. Historically, some implementations have used a fourth - element to carry a linear referencing measure (sometimes denoted as - "M") or a numerical timestamp, but in most situations a parser will - not be able to properly interpret these values. The interpretation - and meaning of additional elements is beyond the scope of this - specification, and additional elements MAY be ignored by parsers. - - - - -Butler, et al. Standards Track [Page 7] - -RFC 7946 GeoJSON August 2016 - - - A line between two positions is a straight Cartesian line, the - shortest line between those two points in the coordinate reference - system (see Section 4). - - In other words, every point on a line that does not cross the - antimeridian between a point (lon0, lat0) and (lon1, lat1) can be - calculated as - - F(lon, lat) = (lon0 + (lon1 - lon0) * t, lat0 + (lat1 - lat0) * t) - - with t being a real number greater than or equal to 0 and smaller - than or equal to 1. Note that this line may markedly differ from the - geodesic path along the curved surface of the reference ellipsoid. - - The same applies to the optional height element with the proviso that - the direction of the height is as specified in the coordinate - reference system. - - Note that, again, this does not mean that a surface with equal height - follows, for example, the curvature of a body of water. Nor is a - surface of equal height perpendicular to a plumb line. - - Examples of positions and geometries are provided in Appendix A, - "Geometry Examples". - -3.1.2. Point - - For type "Point", the "coordinates" member is a single position. - -3.1.3. MultiPoint - - For type "MultiPoint", the "coordinates" member is an array of - positions. - -3.1.4. LineString - - For type "LineString", the "coordinates" member is an array of two or - more positions. - -3.1.5. MultiLineString - - For type "MultiLineString", the "coordinates" member is an array of - LineString coordinate arrays. - - - - - - - - -Butler, et al. Standards Track [Page 8] - -RFC 7946 GeoJSON August 2016 - - -3.1.6. Polygon - - To specify a constraint specific to Polygons, it is useful to - introduce the concept of a linear ring: - - o A linear ring is a closed LineString with four or more positions. - - o The first and last positions are equivalent, and they MUST contain - identical values; their representation SHOULD also be identical. - - o A linear ring is the boundary of a surface or the boundary of a - hole in a surface. - - o A linear ring MUST follow the right-hand rule with respect to the - area it bounds, i.e., exterior rings are counterclockwise, and - holes are clockwise. - - Note: the [GJ2008] specification did not discuss linear ring winding - order. For backwards compatibility, parsers SHOULD NOT reject - Polygons that do not follow the right-hand rule. - - Though a linear ring is not explicitly represented as a GeoJSON - geometry type, it leads to a canonical formulation of the Polygon - geometry type definition as follows: - - o For type "Polygon", the "coordinates" member MUST be an array of - linear ring coordinate arrays. - - o For Polygons with more than one of these rings, the first MUST be - the exterior ring, and any others MUST be interior rings. The - exterior ring bounds the surface, and the interior rings (if - present) bound holes within the surface. - -3.1.7. MultiPolygon - - For type "MultiPolygon", the "coordinates" member is an array of - Polygon coordinate arrays. - -3.1.8. GeometryCollection - - A GeoJSON object with type "GeometryCollection" is a Geometry object. - A GeometryCollection has a member with the name "geometries". The - value of "geometries" is an array. Each element of this array is a - GeoJSON Geometry object. It is possible for this array to be empty. - - - - - - - -Butler, et al. Standards Track [Page 9] - -RFC 7946 GeoJSON August 2016 - - - Unlike the other geometry types described above, a GeometryCollection - can be a heterogeneous composition of smaller Geometry objects. For - example, a Geometry object in the shape of a lowercase roman "i" can - be composed of one point and one LineString. - - GeometryCollections have a different syntax from single type Geometry - objects (Point, LineString, and Polygon) and homogeneously typed - multipart Geometry objects (MultiPoint, MultiLineString, and - MultiPolygon) but have no different semantics. Although a - GeometryCollection object has no "coordinates" member, it does have - coordinates: the coordinates of all its parts belong to the - collection. The "geometries" member of a GeometryCollection - describes the parts of this composition. Implementations SHOULD NOT - apply any additional semantics to the "geometries" array. - - To maximize interoperability, implementations SHOULD avoid nested - GeometryCollections. Furthermore, GeometryCollections composed of a - single part or a number of parts of a single type SHOULD be avoided - when that single part or a single object of multipart type - (MultiPoint, MultiLineString, or MultiPolygon) could be used instead. - -3.1.9. Antimeridian Cutting - - In representing Features that cross the antimeridian, - interoperability is improved by modifying their geometry. Any - geometry that crosses the antimeridian SHOULD be represented by - cutting it in two such that neither part's representation crosses the - antimeridian. - - For example, a line extending from 45 degrees N, 170 degrees E across - the antimeridian to 45 degrees N, 170 degrees W should be cut in two - and represented as a MultiLineString. - - { - "type": "MultiLineString", - "coordinates": [ - [ - [170.0, 45.0], [180.0, 45.0] - ], [ - [-180.0, 45.0], [-170.0, 45.0] - ] - ] - } - - - - - - - - -Butler, et al. Standards Track [Page 10] - -RFC 7946 GeoJSON August 2016 - - - A rectangle extending from 40 degrees N, 170 degrees E across the - antimeridian to 50 degrees N, 170 degrees W should be cut in two and - represented as a MultiPolygon. - - { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [180.0, 40.0], [180.0, 50.0], [170.0, 50.0], - [170.0, 40.0], [180.0, 40.0] - ] - ], - [ - [ - [-170.0, 40.0], [-170.0, 50.0], [-180.0, 50.0], - [-180.0, 40.0], [-170.0, 40.0] - ] - ] - ] - } - -3.1.10. Uncertainty and Precision - - As in [RFC5870], the number of digits of the values in coordinate - positions MUST NOT be interpreted as an indication to the level of - uncertainty. - -3.2. Feature Object - - A Feature object represents a spatially bounded thing. Every Feature - object is a GeoJSON object no matter where it occurs in a GeoJSON - text. - - o A Feature object has a "type" member with the value "Feature". - - o A Feature object has a member with the name "geometry". The value - of the geometry member SHALL be either a Geometry object as - defined above or, in the case that the Feature is unlocated, a - JSON null value. - - o A Feature object has a member with the name "properties". The - value of the properties member is an object (any JSON object or a - JSON null value). - - - - - - - -Butler, et al. Standards Track [Page 11] - -RFC 7946 GeoJSON August 2016 - - - o If a Feature has a commonly used identifier, that identifier - SHOULD be included as a member of the Feature object with the name - "id", and the value of this member is either a JSON string or - number. - -3.3. FeatureCollection Object - - A GeoJSON object with the type "FeatureCollection" is a - FeatureCollection object. A FeatureCollection object has a member - with the name "features". The value of "features" is a JSON array. - Each element of the array is a Feature object as defined above. It - is possible for this array to be empty. - -4. Coordinate Reference System - - The coordinate reference system for all GeoJSON coordinates is a - geographic coordinate reference system, using the World Geodetic - System 1984 (WGS 84) [WGS84] datum, with longitude and latitude units - of decimal degrees. This is equivalent to the coordinate reference - system identified by the Open Geospatial Consortium (OGC) URN - urn:ogc:def:crs:OGC::CRS84. An OPTIONAL third-position element SHALL - be the height in meters above or below the WGS 84 reference - ellipsoid. In the absence of elevation values, applications - sensitive to height or depth SHOULD interpret positions as being at - local ground or sea level. - - Note: the use of alternative coordinate reference systems was - specified in [GJ2008], but it has been removed from this version of - the specification because the use of different coordinate reference - systems -- especially in the manner specified in [GJ2008] -- has - proven to have interoperability issues. In general, GeoJSON - processing software is not expected to have access to coordinate - reference system databases or to have network access to coordinate - reference system transformation parameters. However, where all - involved parties have a prior arrangement, alternative coordinate - reference systems can be used without risk of data being - misinterpreted. - -5. Bounding Box - - A GeoJSON object MAY have a member named "bbox" to include - information on the coordinate range for its Geometries, Features, or - FeatureCollections. The value of the bbox member MUST be an array of - length 2*n where n is the number of dimensions represented in the - contained geometries, with all axes of the most southwesterly point - followed by all axes of the more northeasterly point. The axes order - of a bbox follows the axes order of geometries. - - - - -Butler, et al. Standards Track [Page 12] - -RFC 7946 GeoJSON August 2016 - - - The "bbox" values define shapes with edges that follow lines of - constant longitude, latitude, and elevation. - - Example of a 2D bbox member on a Feature: - - { - "type": "Feature", - "bbox": [-10.0, -10.0, 10.0, 10.0], - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [-10.0, -10.0], - [10.0, -10.0], - [10.0, 10.0], - [-10.0, -10.0] - ] - ] - } - //... - } - - Example of a 2D bbox member on a FeatureCollection: - - { - "type": "FeatureCollection", - "bbox": [100.0, 0.0, 105.0, 1.0], - "features": [ - //... - ] - } - - Example of a 3D bbox member with a depth of 100 meters: - - { - "type": "FeatureCollection", - "bbox": [100.0, 0.0, -100.0, 105.0, 1.0, 0.0], - "features": [ - //... - ] - } - - - - - - - - - - -Butler, et al. Standards Track [Page 13] - -RFC 7946 GeoJSON August 2016 - - -5.1. The Connecting Lines - - The four lines of the bounding box are defined fully within the - coordinate reference system; that is, for a box bounded by the values - "west", "south", "east", and "north", every point on the northernmost - line can be expressed as - - (lon, lat) = (west + (east - west) * t, north) - - with 0 <= t <= 1. - -5.2. The Antimeridian - - Consider a set of point Features within the Fiji archipelago, - straddling the antimeridian between 16 degrees S and 20 degrees S. - The southwest corner of the box containing these Features is at 20 - degrees S and 177 degrees E, and the northwest corner is at 16 - degrees S and 178 degrees W. The antimeridian-spanning GeoJSON - bounding box for this FeatureCollection is - - "bbox": [177.0, -20.0, -178.0, -16.0] - - and covers 5 degrees of longitude. - - The complementary bounding box for the same latitude band, not - crossing the antimeridian, is - - "bbox": [-178.0, -20.0, 177.0, -16.0] - - and covers 355 degrees of longitude. - - The latitude of the northeast corner is always greater than the - latitude of the southwest corner, but bounding boxes that cross the - antimeridian have a northeast corner longitude that is less than the - longitude of the southwest corner. - -5.3. The Poles - - A bounding box that contains the North Pole extends from a southwest - corner of "minlat" degrees N, 180 degrees W to a northeast corner of - 90 degrees N, 180 degrees E. Viewed on a globe, this bounding box - approximates a spherical cap bounded by the "minlat" circle of - latitude. - - "bbox": [-180.0, minlat, 180.0, 90.0] - - - - - - -Butler, et al. Standards Track [Page 14] - -RFC 7946 GeoJSON August 2016 - - - A bounding box that contains the South Pole extends from a southwest - corner of 90 degrees S, 180 degrees W to a northeast corner of - "maxlat" degrees S, 180 degrees E. - - "bbox": [-180.0, -90.0, 180.0, maxlat] - - A bounding box that just touches the North Pole and forms a slice of - an approximate spherical cap when viewed on a globe extends from a - southwest corner of "minlat" degrees N and "westlon" degrees E to a - northeast corner of 90 degrees N and "eastlon" degrees E. - - "bbox": [westlon, minlat, eastlon, 90.0] - - Similarly, a bounding box that just touches the South Pole and forms - a slice of an approximate spherical cap when viewed on a globe has - the following representation in GeoJSON. - - "bbox": [westlon, -90.0, eastlon, maxlat] - - Implementers MUST NOT use latitude values greater than 90 or less - than -90 to imply an extent that is not a spherical cap. - -6. Extending GeoJSON - -6.1. Foreign Members - - Members not described in this specification ("foreign members") MAY - be used in a GeoJSON document. Note that support for foreign members - can vary across implementations, and no normative processing model - for foreign members is defined. Accordingly, implementations that - rely too heavily on the use of foreign members might experience - reduced interoperability with other implementations. - - For example, in the (abridged) Feature object shown below - - { - "type": "Feature", - "id": "f1", - "geometry": {...}, - "properties": {...}, - "title": "Example Feature" - } - - the name/value pair of "title": "Example Feature" is a foreign - member. When the value of a foreign member is an object, all the - descendant members of that object are themselves foreign members. - - - - - -Butler, et al. Standards Track [Page 15] - -RFC 7946 GeoJSON August 2016 - - - GeoJSON semantics do not apply to foreign members and their - descendants, regardless of their names and values. For example, in - the (abridged) Feature object below - - { - "type": "Feature", - "id": "f2", - "geometry": {...}, - "properties": {...}, - "centerline": { - "type": "LineString", - "coordinates": [ - [-170, 10], - [170, 11] - ] - } - } - - the "centerline" member is not a GeoJSON Geometry object. - -7. GeoJSON Types Are Not Extensible - - Implementations MUST NOT extend the fixed set of GeoJSON types: - FeatureCollection, Feature, Point, LineString, MultiPoint, Polygon, - MultiLineString, MultiPolygon, and GeometryCollection. - -7.1. Semantics of GeoJSON Members and Types Are Not Changeable - - Implementations MUST NOT change the semantics of GeoJSON members and - types. - - The GeoJSON "coordinates" and "geometries" members define Geometry - objects. FeatureCollection and Feature objects, respectively, MUST - NOT contain a "coordinates" or "geometries" member. - - The GeoJSON "geometry" and "properties" members define a Feature - object. FeatureCollection and Geometry objects, respectively, MUST - NOT contain a "geometry" or "properties" member. - - The GeoJSON "features" member defines a FeatureCollection object. - Feature and Geometry objects, respectively, MUST NOT contain a - "features" member. - - - - - - - - - -Butler, et al. Standards Track [Page 16] - -RFC 7946 GeoJSON August 2016 - - -8. Versioning - - The GeoJSON format can be extended as defined here, but no explicit - versioning scheme is defined. A specification that alters the - semantics of GeoJSON members or otherwise modifies the format does - not create a new version of this format; instead, it defines an - entirely new format that MUST NOT be identified as GeoJSON. - -9. Mapping 'geo' URIs - - 'geo' URIs [RFC5870] identify geographic locations and precise (not - uncertain) locations can be mapped to GeoJSON Geometry objects. - - For this section, as in [RFC5870], "lat", "lon", "alt", and "unc" are - placeholders for 'geo' URI latitude, longitude, altitude, and - uncertainty values, respectively. - - A 'geo' URI with two coordinates and an uncertainty ('u') parameter - that is absent or zero, and a GeoJSON Point geometry may be mapped to - each other. A GeoJSON Point is always converted to a 'geo' URI that - has no uncertainty parameter. - - 'geo' URI: - - geo:lat,lon - - GeoJSON: - - {"type": "Point", "coordinates": [lon, lat]} - - The mapping between 'geo' URIs and GeoJSON Points that specify - elevation is shown below. - - 'geo' URI: - - geo:lat,lon,alt - - GeoJSON: - - {"type": "Point", "coordinates": [lon, lat, alt]} - - GeoJSON has no concept of uncertainty; imprecise or uncertain 'geo' - URIs thus cannot be mapped to GeoJSON geometries. - - - - - - - - -Butler, et al. Standards Track [Page 17] - -RFC 7946 GeoJSON August 2016 - - -10. Security Considerations - - GeoJSON shares security issues common to all JSON content types. See - [RFC7159], Section 12 for additional information. GeoJSON does not - provide executable content. - - GeoJSON does not provide privacy or integrity services. If sensitive - data requires privacy or integrity protection, those must be provided - by the transport -- for example, Transport Layer Security (TLS) or - HTTPS. There will be cases in which stored data need protection, - which is out of scope for this document. - - As with other geographic data formats, e.g., [KMLv2.2], providing - details about the locations of sensitive persons, animals, habitats, - and facilities can expose them to unauthorized tracking or injury. - Data providers should recognize the risk of inadvertently identifying - individuals if locations in anonymized datasets are not adequately - skewed or not sufficiently fuzzed [Sweeney] and recognize that the - effectiveness of location obscuration is limited by a number of - factors and is unlikely to be an effective defense against a - determined attack [RFC6772]. - -11. Interoperability Considerations - -11.1. I-JSON - - GeoJSON texts should follow the constraints of Internet JSON (I-JSON) - [RFC7493] for maximum interoperability. - -11.2. Coordinate Precision - - The size of a GeoJSON text in bytes is a major interoperability - consideration, and precision of coordinate values has a large impact - on the size of texts. A GeoJSON text containing many detailed - Polygons can be inflated almost by a factor of two by increasing - coordinate precision from 6 to 15 decimal places. For geographic - coordinates with units of degrees, 6 decimal places (a default common - in, e.g., sprintf) amounts to about 10 centimeters, a precision well - within that of current GPS systems. Implementations should consider - the cost of using a greater precision than necessary. - - Furthermore, the WGS 84 [WGS84] datum is a relatively coarse - approximation of the geoid, with the height varying by up to 5 m (but - generally between 2 and 3 meters) higher or lower relative to a - surface parallel to Earth's mean sea level. - - - - - - -Butler, et al. Standards Track [Page 18] - -RFC 7946 GeoJSON August 2016 - - -12. IANA Considerations - - The media type for GeoJSON text is "application/geo+json" and is - registered in the "Media Types" registry described in [RFC6838]. The - entry for "application/vnd.geo+json" in the same registry should have - its status changed to be "OBSOLETED" with a pointer to the media type - "application/geo+json" and a reference added to this RFC. - - Type name: application - - Subtype name: geo+json - - Required parameters: n/a - - Optional parameters: n/a - - Encoding considerations: binary - - Security considerations: See Section 10 above - - Interoperability considerations: See Section 11 above - - Published specification: [[RFC7946]] - - Applications that use this media type: No known applications - currently use this media type. This media type is intended for - GeoJSON applications currently using the "application/ - vnd.geo+json" or "application/json" media types, of which there - are several categories: web mapping, geospatial databases, - geographic data processing APIs, data analysis and storage - services, and data dissemination. - - Additional information: - - Magic number(s): n/a - - File extension(s): .json, .geojson - - Macintosh file type code: n/a - - Object Identifiers: n/a - - Windows clipboard name: GeoJSON - - Macintosh uniform type identifier: public.geojson conforms to - public.json - - - - - -Butler, et al. Standards Track [Page 19] - -RFC 7946 GeoJSON August 2016 - - - Person to contact for further information: Sean Gillies - (sean.gillies@gmail.com) - - Intended usage: COMMON - - Restrictions on usage: none - - Restrictions on usage: none - - Author: see "Authors' Addresses" section of [[RFC7946]]. - - Change controller: Internet Engineering Task Force - -13. References - -13.1. Normative References - - [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate - Requirement Levels", BCP 14, RFC 2119, - DOI 10.17487/RFC2119, March 1997, - . - - [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type - Specifications and Registration Procedures", BCP 13, - RFC 6838, DOI 10.17487/RFC6838, January 2013, - . - - [RFC7159] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data - Interchange Format", RFC 7159, DOI 10.17487/RFC7159, March - 2014, . - - [RFC7493] Bray, T., Ed., "The I-JSON Message Format", RFC 7493, - DOI 10.17487/RFC7493, March 2015, - . - - [WGS84] National Imagery and Mapping Agency, "Department of - Defense World Geodetic System 1984: Its Definition and - Relationships with Local Geodetic Systems", Third Edition, - 1984. - - - - - - - - - - - - -Butler, et al. Standards Track [Page 20] - -RFC 7946 GeoJSON August 2016 - - -13.2. Informative References - - [GJ2008] Butler, H., Daly, M., Doyle, A., Gillies, S., Schaub, T., - and C. Schmidt, "The GeoJSON Format Specification", June - 2008. - - [KMLv2.2] Wilson, T., "OGC KML", OGC 07-147r2, Version 2.2.0, April - 2008. - - [RFC5870] Mayrhofer, A. and C. Spanring, "A Uniform Resource - Identifier for Geographic Locations ('geo' URI)", - RFC 5870, DOI 10.17487/RFC5870, June 2010, - . - - [RFC6772] Schulzrinne, H., Ed., Tschofenig, H., Ed., Cuellar, J., - Polk, J., Morris, J., and M. Thomson, "Geolocation Policy: - A Document Format for Expressing Privacy Preferences for - Location Information", RFC 6772, DOI 10.17487/RFC6772, - January 2013, . - - [RFC7464] Williams, N., "JavaScript Object Notation (JSON) Text - Sequences", RFC 7464, DOI 10.17487/RFC7464, February 2015, - . - - [SFSQL] OpenGIS Consortium, Inc., "OpenGIS Simple Features - Specification For SQL Revision 1.1", OGC 99-049, May 1999. - - [Sweeney] Sweeney, L., "k-anonymity: a model for protecting - privacy", International Journal on Uncertainty, Fuzziness - and Knowledge-based Systems 10 (5), 2002; 557-570, - DOI 10.1142/S0218488502001648, 2002. - - [WFSv1] Vretanos, P., "Web Feature Service Implementation - Specification", OGC 04-094, Version 1.1.0, May 2005. - - - - - - - - - - - - - - - - - -Butler, et al. Standards Track [Page 21] - -RFC 7946 GeoJSON August 2016 - - -Appendix A. Geometry Examples - - Each of the examples below represents a valid and complete GeoJSON - object. - -A.1. Points - - Point coordinates are in x, y order (easting, northing for projected - coordinates, longitude, and latitude for geographic coordinates): - - { - "type": "Point", - "coordinates": [100.0, 0.0] - } - -A.2. LineStrings - - Coordinates of LineString are an array of positions (see - Section 3.1.1): - - { - "type": "LineString", - "coordinates": [ - [100.0, 0.0], - [101.0, 1.0] - ] - } - - - - - - - - - - - - - - - - - - - - - - - - -Butler, et al. Standards Track [Page 22] - -RFC 7946 GeoJSON August 2016 - - -A.3. Polygons - - Coordinates of a Polygon are an array of linear ring (see - Section 3.1.6) coordinate arrays. The first element in the array - represents the exterior ring. Any subsequent elements represent - interior rings (or holes). - - No holes: - - { - "type": "Polygon", - "coordinates": [ - [ - [100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0] - ] - ] - } - - With holes: - - { - "type": "Polygon", - "coordinates": [ - [ - [100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0] - ], - [ - [100.8, 0.8], - [100.8, 0.2], - [100.2, 0.2], - [100.2, 0.8], - [100.8, 0.8] - ] - ] - } - - - - - - - - -Butler, et al. Standards Track [Page 23] - -RFC 7946 GeoJSON August 2016 - - -A.4. MultiPoints - - Coordinates of a MultiPoint are an array of positions: - - { - "type": "MultiPoint", - "coordinates": [ - [100.0, 0.0], - [101.0, 1.0] - ] - } - -A.5. MultiLineStrings - - Coordinates of a MultiLineString are an array of LineString - coordinate arrays: - - { - "type": "MultiLineString", - "coordinates": [ - [ - [100.0, 0.0], - [101.0, 1.0] - ], - [ - [102.0, 2.0], - [103.0, 3.0] - ] - ] - } - - - - - - - - - - - - - - - - - - - - - -Butler, et al. Standards Track [Page 24] - -RFC 7946 GeoJSON August 2016 - - -A.6. MultiPolygons - - Coordinates of a MultiPolygon are an array of Polygon coordinate - arrays: - - { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [102.0, 2.0], - [103.0, 2.0], - [103.0, 3.0], - [102.0, 3.0], - [102.0, 2.0] - ] - ], - [ - [ - [100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0] - ], - [ - [100.2, 0.2], - [100.2, 0.8], - [100.8, 0.8], - [100.8, 0.2], - [100.2, 0.2] - ] - ] - ] - } - - - - - - - - - - - - - - - - -Butler, et al. Standards Track [Page 25] - -RFC 7946 GeoJSON August 2016 - - -A.7. GeometryCollections - - Each element in the "geometries" array of a GeometryCollection is one - of the Geometry objects described above: - - { - "type": "GeometryCollection", - "geometries": [{ - "type": "Point", - "coordinates": [100.0, 0.0] - }, { - "type": "LineString", - "coordinates": [ - [101.0, 0.0], - [102.0, 1.0] - ] - }] - } - -Appendix B. Changes from the Pre-IETF GeoJSON Format Specification - - This appendix briefly summarizes non-editorial changes from the 2008 - specification [GJ2008]. - -B.1. Normative Changes - - o Specification of coordinate reference systems has been removed, - i.e., the "crs" member of [GJ2008] is no longer used. - - o In the absence of elevation values, applications sensitive to - height or depth SHOULD interpret positions as being at local - ground or sea level (see Section 4). - - o Implementations SHOULD NOT extend position arrays beyond 3 - elements (see Section 3.1.1). - - o A line between two positions is a straight Cartesian line (see - Section 3.1.1). - - o Polygon rings MUST follow the right-hand rule for orientation - (counterclockwise external rings, clockwise internal rings). - - o The values of a "bbox" array are "[west, south, east, north]", not - "[minx, miny, maxx, maxy]" (see Section 5). - - o A Feature object's "id" member is a string or number (see - Section 3.2). - - - - -Butler, et al. Standards Track [Page 26] - -RFC 7946 GeoJSON August 2016 - - - o Extensions MAY be used, but MUST NOT change the semantics of - GeoJSON members and types (see Section 6). - - o GeoJSON objects MUST NOT contain the defining members of other - types (see Section 7.1). - - o The media type for GeoJSON is "application/geo+json". - -B.2. Informative Changes - - o The definition of a GeoJSON text has been added. - - o Rules for mapping 'geo' URIs have been added. - - o A recommendation of the I-JSON [RFC7493] constraints has been - added. - - o Implementers are cautioned about the effect of excessive - coordinate precision on interoperability. - - o Interoperability concerns of GeometryCollections are noted. These - objects should be used sparingly (see Section 3.1.8). - -Appendix C. GeoJSON Text Sequences - - All GeoJSON objects defined in this specification -- - FeatureCollection, Feature, and Geometry -- consist of exactly one - JSON object. However, there may be circumstances in which - applications need to represent sets or sequences of these objects - (over and above the grouping of Feature objects in a - FeatureCollection), e.g., in order to efficiently "stream" large - numbers of Feature objects. The definition of such sets or sequences - is outside the scope of this specification. - - If such a representation is needed, a new media type is required that - has the ability to represent these sets or sequences. When defining - such a media type, it may be useful to base it on "JavaScript Object - Notation (JSON) Text Sequences" [RFC7464], leaving the foundations of - how to represent multiple JSON objects to that specification, and - only defining how it applies to GeoJSON objects. - -Acknowledgements - - The GeoJSON format is the product of discussion on the GeoJSON - mailing list, , before October 2015 and in the IETF's GeoJSON - WG after October 2015. - - - - -Butler, et al. Standards Track [Page 27] - -RFC 7946 GeoJSON August 2016 - - - Material in this document was adapted with changes from - [GJ2008], which is licensed - under . - -Authors' Addresses - - Howard Butler - Hobu Inc. - - Email: howard@hobu.co - - - Martin Daly - Cadcorp - - Email: martin.daly@cadcorp.com - - - Allan Doyle - - Email: adoyle@intl-interfaces.com - - - Sean Gillies - Mapbox - - Email: sean.gillies@gmail.com - URI: http://sgillies.net - - - Stefan Hagen - Rheinaustr. 62 - Bonn 53225 - Germany - - Email: stefan@hagen.link - URI: http://stefan-hagen.website/ - - - Tim Schaub - Planet Labs - - Email: tim.schaub@gmail.com - - - - - - - - -Butler, et al. Standards Track [Page 28] - diff --git a/phpdoc.xml b/phpdoc.xml deleted file mode 100644 index 065f870..0000000 --- a/phpdoc.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - phpGPX API Documentation - - - docs-output - - - - - - src/phpGPX - - api - - phpGPX - - - \ No newline at end of file diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index 1a523fb..d1e9eca 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -26,7 +26,7 @@ class phpGPX const GEOJSON_FORMAT = 'geojson'; const PACKAGE_NAME = 'phpGPX'; - const VERSION = '2.0.0-alpha.3'; + const VERSION = '2.0.0-beta.1'; public readonly Config $config; From d7c31b3c6df6fbd794bcdafce293ad130c873164 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 11:21:05 +0100 Subject: [PATCH 30/31] =?UTF-8?q?Codestyle=20=F0=9F=A4=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/CreateFileFromScratch.php | 27 +++++----- examples/Example.php | 19 +++---- examples/waypoints_create.php | 19 +++---- examples/waypoints_load.php | 7 +-- src/phpGPX/Analysis/AbstractPointAnalyzer.php | 2 +- src/phpGPX/Analysis/AltitudeAnalyzer.php | 5 +- src/phpGPX/Analysis/BoundsAnalyzer.php | 2 +- src/phpGPX/Analysis/DistanceAnalyzer.php | 5 +- src/phpGPX/Analysis/ElevationAnalyzer.php | 5 +- src/phpGPX/Analysis/Engine.php | 9 ++-- src/phpGPX/Analysis/MovementAnalyzer.php | 5 +- .../Analysis/PointAnalyzerInterface.php | 2 +- src/phpGPX/Analysis/TimestampAnalyzer.php | 2 +- .../Analysis/TrackPointExtensionAnalyzer.php | 2 +- src/phpGPX/Config.php | 5 +- src/phpGPX/Helpers/DateTimeHelper.php | 31 +++++------ src/phpGPX/Helpers/DistanceCalculator.php | 6 ++- .../Helpers/ElevationGainLossCalculator.php | 3 +- src/phpGPX/Helpers/GeoHelper.php | 7 +-- src/phpGPX/Helpers/SerializationHelper.php | 1 + src/phpGPX/Models/Bounds.php | 7 +-- src/phpGPX/Models/Collection.php | 2 +- src/phpGPX/Models/Copyright.php | 7 +-- src/phpGPX/Models/Email.php | 7 +-- src/phpGPX/Models/Extensions.php | 2 +- .../Models/Extensions/ExtensionInterface.php | 2 +- .../Models/Extensions/TrackPointExtension.php | 26 +++++++--- src/phpGPX/Models/GpxFile.php | 28 +++++----- src/phpGPX/Models/Link.php | 7 +-- src/phpGPX/Models/Metadata.php | 4 +- src/phpGPX/Models/Person.php | 7 +-- src/phpGPX/Models/Point.php | 7 +-- src/phpGPX/Models/PointType.php | 2 +- src/phpGPX/Models/Route.php | 6 +-- src/phpGPX/Models/Segment.php | 4 +- src/phpGPX/Models/Stats.php | 4 +- src/phpGPX/Models/Track.php | 6 +-- src/phpGPX/Parsers/AbstractParser.php | 2 +- src/phpGPX/Parsers/BoundsParser.php | 13 ++--- src/phpGPX/Parsers/CopyrightParser.php | 1 + src/phpGPX/Parsers/EmailParser.php | 1 + src/phpGPX/Parsers/ExtensionParser.php | 2 +- src/phpGPX/Parsers/ExtensionRegistry.php | 2 +- .../Extensions/ExtensionParserInterface.php | 2 +- .../Extensions/TrackPointExtensionParser.php | 27 +++++----- src/phpGPX/Parsers/LinkParser.php | 3 +- src/phpGPX/Parsers/MetadataParser.php | 13 ++--- src/phpGPX/Parsers/PersonParser.php | 1 + src/phpGPX/Parsers/PointParser.php | 38 +++++++------- src/phpGPX/Parsers/RouteParser.php | 15 +++--- src/phpGPX/Parsers/SegmentParser.php | 3 +- src/phpGPX/Parsers/TrackParser.php | 15 +++--- src/phpGPX/Parsers/WaypointParser.php | 2 +- src/phpGPX/phpGPX.php | 15 +++--- tests/Integration/GeoJsonOutputTest.php | 2 +- tests/Integration/GpxFileLoadTest.php | 2 +- tests/Integration/XmlRoundTripTest.php | 16 +++--- tests/Unit/Analysis/BoundsAnalyzerTest.php | 3 +- tests/Unit/Analysis/EngineTest.php | 51 ++++++++++++------- tests/Unit/Analysis/MovementAnalyzerTest.php | 4 +- .../TrackPointExtensionAnalyzerTest.php | 4 +- tests/Unit/Helpers/DateTimeHelperTest.php | 26 +++++----- tests/Unit/Helpers/DistanceCalculatorTest.php | 2 +- .../ElevationGainLossCalculatorTest.php | 2 +- tests/Unit/Helpers/GeoHelperTest.php | 8 +-- .../Unit/Helpers/SerializationHelperTest.php | 2 +- tests/Unit/Models/BoundsTest.php | 4 +- tests/Unit/Models/StatsCalculationTest.php | 6 +-- tests/Unit/Parsers/BoundsParserTest.php | 8 +-- tests/Unit/Parsers/CopyrightParserTest.php | 13 ++--- tests/Unit/Parsers/EmailParserTest.php | 13 ++--- tests/Unit/Parsers/ExtensionParserTest.php | 12 +++-- tests/Unit/Parsers/ExtensionRegistryTest.php | 4 +- tests/Unit/Parsers/LinkParserTest.php | 15 +++--- tests/Unit/Parsers/PersonParserTest.php | 23 +++++---- 75 files changed, 367 insertions(+), 300 deletions(-) diff --git a/examples/CreateFileFromScratch.php b/examples/CreateFileFromScratch.php index 18f5473..30a31e3 100644 --- a/examples/CreateFileFromScratch.php +++ b/examples/CreateFileFromScratch.php @@ -1,8 +1,11 @@ */ +use phpGPX\Models\Extensions; +use phpGPX\Models\Extensions\TrackPointExtension; use phpGPX\Models\GpxFile; use phpGPX\Models\Link; use phpGPX\Models\Metadata; @@ -10,8 +13,6 @@ use phpGPX\Models\PointType; use phpGPX\Models\Segment; use phpGPX\Models\Track; -use phpGPX\Models\Extensions; -use phpGPX\Models\Extensions\TrackPointExtension; require_once '../vendor/autoload.php'; @@ -21,34 +22,34 @@ 'latitude' => 54.9328621088893, 'elevation' => 0, 'aTemp' => 22, - 'time' => new \DateTime("+ 1 MINUTE") + 'time' => new \DateTime('+ 1 MINUTE'), ], [ 'latitude' => 54.83293237320851, 'longitude' => 9.76092208681491, 'elevation' => 10.0, 'aTemp' => 23, - 'time' => new \DateTime("+ 2 MINUTE") + 'time' => new \DateTime('+ 2 MINUTE'), ], [ 'latitude' => 54.73327743521187, 'longitude' => 9.66187816543752, 'elevation' => 42.42, 'aTemp' => 24, - 'time' => new \DateTime("+ 3 MINUTE") + 'time' => new \DateTime('+ 3 MINUTE'), ], [ 'latitude' => 54.63342326167919, 'longitude' => 9.562439849679859, 'elevation' => 12, 'aTemp' => 25, - 'time' => new \DateTime("+ 4 MINUTE") - ] + 'time' => new \DateTime('+ 4 MINUTE'), + ], ]; // Creating sample link object for metadata $link = new Link(); -$link->href = "https://sibyx.github.io/phpgpx"; +$link->href = 'https://sibyx.github.io/phpgpx'; $link->text = 'phpGPX Docs'; // GpxFile contains data and handles serialization of objects @@ -61,7 +62,7 @@ $gpx_file->metadata->time = new \DateTime(); // Description of GPX file -$gpx_file->metadata->description = "My pretty awesome GPX file, created using phpGPX library!"; +$gpx_file->metadata->description = 'My pretty awesome GPX file, created using phpGPX library!'; // Adding link created before to links array of metadata // Metadata of GPX file can contain more than one link @@ -71,13 +72,13 @@ $track = new Track(); // Name of track -$track->name = sprintf("Some random points in logical order. Input array should be already ordered!"); +$track->name = sprintf('Some random points in logical order. Input array should be already ordered!'); // Type of data stored in track $track->type = 'RUN'; // Source of GPS coordinates -$track->source = sprintf("MySpecificGarminDevice"); +$track->source = sprintf('MySpecificGarminDevice'); // Creating Track segment $segment = new Segment(); @@ -125,8 +126,8 @@ // Direct GPX output to browser -header("Content-Type: application/gpx+xml"); -header("Content-Disposition: attachment; filename=CreatingFileFromScratchExample.gpx"); +header('Content-Type: application/gpx+xml'); +header('Content-Disposition: attachment; filename=CreatingFileFromScratchExample.gpx'); echo $gpx_file->toXML()->saveXML(); exit(); diff --git a/examples/Example.php b/examples/Example.php index 56abd4f..a3cdc20 100644 --- a/examples/Example.php +++ b/examples/Example.php @@ -1,12 +1,13 @@ */ -use phpGPX\phpGPX; -use phpGPX\Config; use phpGPX\Analysis\Engine; +use phpGPX\Config; +use phpGPX\phpGPX; require_once '../vendor/autoload.php'; @@ -18,12 +19,12 @@ $file = $gpx->load('endomondo.gpx'); foreach ($file->tracks as $track) { - echo "Track: " . $track->name . "\n"; - echo "Distance: " . round($track->stats->distance) . " m\n"; - echo "Duration: " . $track->stats->duration . " s\n"; - echo "Avg speed: " . round($track->stats->averageSpeed, 2) . " m/s\n"; - echo "Elevation gain: " . round($track->stats->cumulativeElevationGain, 1) . " m\n"; - echo "Elevation loss: " . round($track->stats->cumulativeElevationLoss, 1) . " m\n"; + echo 'Track: ' . $track->name . "\n"; + echo 'Distance: ' . round($track->stats->distance) . " m\n"; + echo 'Duration: ' . $track->stats->duration . " s\n"; + echo 'Avg speed: ' . round($track->stats->averageSpeed, 2) . " m/s\n"; + echo 'Elevation gain: ' . round($track->stats->cumulativeElevationGain, 1) . " m\n"; + echo 'Elevation loss: ' . round($track->stats->cumulativeElevationLoss, 1) . " m\n"; echo "\nFull stats:\n"; var_dump($track->stats->jsonSerialize()); -} \ No newline at end of file +} diff --git a/examples/waypoints_create.php b/examples/waypoints_create.php index 394f2fb..2aba9a3 100644 --- a/examples/waypoints_create.php +++ b/examples/waypoints_create.php @@ -1,4 +1,5 @@ */ @@ -18,31 +19,31 @@ 'longitude' => 9.860624216140083, 'latitude' => 54.9328621088893, 'elevation' => 0, - 'time' => new \DateTime("+ 1 MINUTE") + 'time' => new \DateTime('+ 1 MINUTE'), ], [ 'latitude' => 54.83293237320851, 'longitude' => 9.76092208681491, 'elevation' => 10.0, - 'time' => new \DateTime("+ 2 MINUTE") + 'time' => new \DateTime('+ 2 MINUTE'), ], [ 'latitude' => 54.73327743521187, 'longitude' => 9.66187816543752, 'elevation' => 42.42, - 'time' => new \DateTime("+ 3 MINUTE") + 'time' => new \DateTime('+ 3 MINUTE'), ], [ 'latitude' => 54.63342326167919, 'longitude' => 9.562439849679859, 'elevation' => 12, - 'time' => new \DateTime("+ 4 MINUTE") - ] + 'time' => new \DateTime('+ 4 MINUTE'), + ], ]; // Creating sample link object for metadata $link = new Link(); -$link->href = "https://sibyx.github.io/phpgpx"; +$link->href = 'https://sibyx.github.io/phpgpx'; $link->text = 'phpGPX Docs'; // GpxFile contains data and handles serialization of objects @@ -55,7 +56,7 @@ $gpx_file->metadata->time = new \DateTime(); // Description of GPX file -$gpx_file->metadata->description = "My pretty awesome GPX file, created using phpGPX library!"; +$gpx_file->metadata->description = 'My pretty awesome GPX file, created using phpGPX library!'; // Adding link created before to links array of metadata // Metadata of GPX file can contain more than one link @@ -65,13 +66,13 @@ $track = new Track(); // Name of track -$track->name = sprintf("Some random points in logical order. Input array should be already ordered!"); +$track->name = sprintf('Some random points in logical order. Input array should be already ordered!'); // Type of data stored in track $track->type = 'RUN'; // Source of GPS coordinates -$track->source = sprintf("MySpecificGarminDevice"); +$track->source = sprintf('MySpecificGarminDevice'); $wp = []; foreach ($sample_data as $sample_point) { diff --git a/examples/waypoints_load.php b/examples/waypoints_load.php index 02dc4ee..89e76e1 100644 --- a/examples/waypoints_load.php +++ b/examples/waypoints_load.php @@ -1,11 +1,12 @@ */ -use phpGPX\phpGPX; use phpGPX\Config; +use phpGPX\phpGPX; require_once '../vendor/autoload.php'; @@ -21,7 +22,7 @@ system("diff $origFile $outFile", $retcode); if ($retcode != 0) { - throw new \Exception("waypoint file incorrect"); + throw new \Exception('waypoint file incorrect'); } else { print "waypoint test successful\n"; -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/AbstractPointAnalyzer.php b/src/phpGPX/Analysis/AbstractPointAnalyzer.php index eb53c19..d8a9c28 100644 --- a/src/phpGPX/Analysis/AbstractPointAnalyzer.php +++ b/src/phpGPX/Analysis/AbstractPointAnalyzer.php @@ -55,4 +55,4 @@ public function finalizeFile(GpxFile $gpxFile): void { // No-op by default — override for file-level post-processing. } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/AltitudeAnalyzer.php b/src/phpGPX/Analysis/AltitudeAnalyzer.php index 61b74da..7e6b493 100644 --- a/src/phpGPX/Analysis/AltitudeAnalyzer.php +++ b/src/phpGPX/Analysis/AltitudeAnalyzer.php @@ -32,7 +32,8 @@ class AltitudeAnalyzer extends AbstractPointAnalyzer public function __construct( private bool $ignoreZeroElevation = false, - ) {} + ) { + } public function begin(): void { @@ -99,4 +100,4 @@ public function aggregateTrack(Track $track): void } } } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/BoundsAnalyzer.php b/src/phpGPX/Analysis/BoundsAnalyzer.php index 2b4c78a..587999b 100644 --- a/src/phpGPX/Analysis/BoundsAnalyzer.php +++ b/src/phpGPX/Analysis/BoundsAnalyzer.php @@ -175,4 +175,4 @@ private function expandFileBounds(float $lat, float $lon): void $this->fileMaxLon = $lon; } } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/DistanceAnalyzer.php b/src/phpGPX/Analysis/DistanceAnalyzer.php index 44174dd..2d0cf59 100644 --- a/src/phpGPX/Analysis/DistanceAnalyzer.php +++ b/src/phpGPX/Analysis/DistanceAnalyzer.php @@ -44,7 +44,8 @@ class DistanceAnalyzer extends AbstractPointAnalyzer public function __construct( private bool $applySmoothing = false, private int $smoothingThreshold = 2, - ) {} + ) { + } public function begin(): void { @@ -103,4 +104,4 @@ public function aggregateTrack(Track $track): void $track->stats->distance = $totalRaw; $track->stats->realDistance = $totalReal; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/ElevationAnalyzer.php b/src/phpGPX/Analysis/ElevationAnalyzer.php index 2fc31f3..333e861 100644 --- a/src/phpGPX/Analysis/ElevationAnalyzer.php +++ b/src/phpGPX/Analysis/ElevationAnalyzer.php @@ -48,7 +48,8 @@ public function __construct( private bool $applySmoothing = false, private int $smoothingThreshold = 2, private ?int $spikesThreshold = null, - ) {} + ) { + } public function begin(): void { @@ -115,4 +116,4 @@ public function aggregateTrack(Track $track): void $track->stats->cumulativeElevationGain = $totalGain; $track->stats->cumulativeElevationLoss = $totalLoss; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/Engine.php b/src/phpGPX/Analysis/Engine.php index ead9851..225adc3 100644 --- a/src/phpGPX/Analysis/Engine.php +++ b/src/phpGPX/Analysis/Engine.php @@ -80,7 +80,8 @@ class Engine */ public function __construct( private bool $sortByTimestamp = false, - ) {} + ) { + } /** * Register an analyzer to participate in the single-pass computation. @@ -249,7 +250,7 @@ private function analyzePoints(array $points, Stats $stats): void */ private function sortPoints(GpxFile $gpxFile): void { - $compare = fn($a, $b) => $a->time <=> $b->time; + $compare = fn ($a, $b) => $a->time <=> $b->time; foreach ($gpxFile->tracks as $track) { foreach ($track->segments as $segment) { @@ -288,7 +289,7 @@ private function computeDerivedStats(Stats $stats): void { if ($stats->startedAt instanceof \DateTime && $stats->finishedAt instanceof \DateTime) { $stats->duration = abs( - $stats->finishedAt->getTimestamp() - $stats->startedAt->getTimestamp() + $stats->finishedAt->getTimestamp() - $stats->startedAt->getTimestamp(), ); if ($stats->duration != 0 && $stats->distance !== null) { @@ -304,4 +305,4 @@ private function computeDerivedStats(Stats $stats): void $stats->movingAverageSpeed = $stats->distance / $stats->movingDuration; } } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/MovementAnalyzer.php b/src/phpGPX/Analysis/MovementAnalyzer.php index 35371d9..8b9acc6 100644 --- a/src/phpGPX/Analysis/MovementAnalyzer.php +++ b/src/phpGPX/Analysis/MovementAnalyzer.php @@ -37,7 +37,8 @@ class MovementAnalyzer extends AbstractPointAnalyzer public function __construct( private float $speedThreshold = 0.5, - ) {} + ) { + } public function begin(): void { @@ -96,4 +97,4 @@ public function aggregateTrack(Track $track): void $track->stats->movingDuration = $hasData ? $totalMoving : null; // movingAverageSpeed is computed by the engine } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/PointAnalyzerInterface.php b/src/phpGPX/Analysis/PointAnalyzerInterface.php index cb67a96..9ea3e73 100644 --- a/src/phpGPX/Analysis/PointAnalyzerInterface.php +++ b/src/phpGPX/Analysis/PointAnalyzerInterface.php @@ -87,4 +87,4 @@ public function aggregateTrack(Track $track): void; * @param GpxFile $gpxFile The fully processed GPX file */ public function finalizeFile(GpxFile $gpxFile): void; -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/TimestampAnalyzer.php b/src/phpGPX/Analysis/TimestampAnalyzer.php index f9d8fee..a8ea7bb 100644 --- a/src/phpGPX/Analysis/TimestampAnalyzer.php +++ b/src/phpGPX/Analysis/TimestampAnalyzer.php @@ -88,4 +88,4 @@ public function aggregateTrack(Track $track): void } } } -} \ No newline at end of file +} diff --git a/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php index cfeceb9..8e6661e 100644 --- a/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php +++ b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php @@ -151,4 +151,4 @@ private function resetTrackAccumulators(): void $this->trackTempSum = 0; $this->trackTempCount = 0; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Config.php b/src/phpGPX/Config.php index bccfce6..4e880da 100644 --- a/src/phpGPX/Config.php +++ b/src/phpGPX/Config.php @@ -12,5 +12,6 @@ class Config public function __construct( /** Pretty print XML and JSON output */ public bool $prettyPrint = true, - ) {} -} \ No newline at end of file + ) { + } +} diff --git a/src/phpGPX/Helpers/DateTimeHelper.php b/src/phpGPX/Helpers/DateTimeHelper.php index 2110b3a..54f6e53 100644 --- a/src/phpGPX/Helpers/DateTimeHelper.php +++ b/src/phpGPX/Helpers/DateTimeHelper.php @@ -1,4 +1,5 @@ @@ -12,15 +13,15 @@ */ class DateTimeHelper { - /** - * @param $datetime - * @param string $format - * @param string|null $timezone - * @return null|string - * @throws \Exception - */ + /** + * @param $datetime + * @param string $format + * @param string|null $timezone + * @return null|string + * @throws \Exception + */ public static function formatDateTime($datetime, string $format = 'c', ?string $timezone = 'UTC'): ?string - { + { $formatted = null; if ($datetime instanceof \DateTime) { @@ -31,14 +32,14 @@ public static function formatDateTime($datetime, string $format = 'c', ?string $ return $formatted; } - /** - * @param $value - * @param string $timezone - * @return \DateTime - * @throws \Exception - */ + /** + * @param $value + * @param string $timezone + * @return \DateTime + * @throws \Exception + */ public static function parseDateTime($value, string $timezone = 'Europe/London'): \DateTime - { + { $timezone = new \DateTimeZone($timezone); $datetime = new \DateTime($value, $timezone); $datetime->setTimezone(new \DateTimeZone(date_default_timezone_get())); diff --git a/src/phpGPX/Helpers/DistanceCalculator.php b/src/phpGPX/Helpers/DistanceCalculator.php index 9a8ecf3..c392acd 100644 --- a/src/phpGPX/Helpers/DistanceCalculator.php +++ b/src/phpGPX/Helpers/DistanceCalculator.php @@ -1,4 +1,5 @@ @@ -15,7 +16,7 @@ */ abstract class GeoHelper { - const EARTH_RADIUS = 6371000; + public const EARTH_RADIUS = 6371000; /** * Returns distance in meters between two Points according to GPX coordinates. @@ -25,7 +26,7 @@ abstract class GeoHelper * @return float */ public static function getRawDistance(Point $point1, Point $point2): float - { + { $latFrom = deg2rad($point1->latitude); $lonFrom = deg2rad($point1->longitude); $latTo = deg2rad($point2->latitude); @@ -46,7 +47,7 @@ public static function getRawDistance(Point $point1, Point $point2): float * @return float */ public static function getRealDistance(Point $point1, Point $point2): float - { + { $distance = self::getRawDistance($point1, $point2); $elevation1 = $point1->elevation != null ? $point1->elevation : 0; diff --git a/src/phpGPX/Helpers/SerializationHelper.php b/src/phpGPX/Helpers/SerializationHelper.php index 4a9df7f..239efd5 100644 --- a/src/phpGPX/Helpers/SerializationHelper.php +++ b/src/phpGPX/Helpers/SerializationHelper.php @@ -1,4 +1,5 @@ diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php index 3fe1479..88ffe4d 100644 --- a/src/phpGPX/Models/Bounds.php +++ b/src/phpGPX/Models/Bounds.php @@ -14,7 +14,8 @@ public function __construct( public ?float $minLongitude = null, public ?float $maxLatitude = null, public ?float $maxLongitude = null, - ) {} + ) { + } /** * GeoJSON bbox: [minLon, minLat, maxLon, maxLat] @@ -34,7 +35,7 @@ public static function parse(\SimpleXMLElement $node): ?Bounds (float) $node['minlat'], (float) $node['minlon'], (float) $node['maxlat'], - (float) $node['maxlon'] + (float) $node['maxlon'], ); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Collection.php b/src/phpGPX/Models/Collection.php index b5c6842..6875962 100644 --- a/src/phpGPX/Models/Collection.php +++ b/src/phpGPX/Models/Collection.php @@ -36,4 +36,4 @@ abstract class Collection implements \JsonSerializable /** @return Point[] */ abstract public function getPoints(): array; -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Copyright.php b/src/phpGPX/Models/Copyright.php index 09507ac..c344527 100644 --- a/src/phpGPX/Models/Copyright.php +++ b/src/phpGPX/Models/Copyright.php @@ -11,7 +11,8 @@ public function __construct( public ?string $author = null, public ?string $year = null, public ?string $license = null, - ) {} + ) { + } public function jsonSerialize(): array { @@ -19,6 +20,6 @@ public function jsonSerialize(): array 'author' => $this->author, 'year' => $this->year, 'license' => $this->license, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Email.php b/src/phpGPX/Models/Email.php index de290ae..5488e6b 100644 --- a/src/phpGPX/Models/Email.php +++ b/src/phpGPX/Models/Email.php @@ -10,13 +10,14 @@ class Email implements \JsonSerializable public function __construct( public ?string $id = null, public ?string $domain = null, - ) {} + ) { + } public function jsonSerialize(): array { return array_filter([ 'id' => $this->id, 'domain' => $this->domain, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Extensions.php b/src/phpGPX/Models/Extensions.php index d2df082..c4db456 100644 --- a/src/phpGPX/Models/Extensions.php +++ b/src/phpGPX/Models/Extensions.php @@ -97,4 +97,4 @@ public function jsonSerialize(): mixed return !empty($result) ? $result : new \stdClass(); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Extensions/ExtensionInterface.php b/src/phpGPX/Models/Extensions/ExtensionInterface.php index 685003d..792eac1 100644 --- a/src/phpGPX/Models/Extensions/ExtensionInterface.php +++ b/src/phpGPX/Models/Extensions/ExtensionInterface.php @@ -54,4 +54,4 @@ public static function getSchemaLocation(): string; * Example: `TrackPointExtension` */ public static function getTagName(): string; -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php index 9b2f452..749988e 100644 --- a/src/phpGPX/Models/Extensions/TrackPointExtension.php +++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php @@ -1,4 +1,5 @@ @@ -15,13 +16,22 @@ */ class TrackPointExtension implements ExtensionInterface { - const NAMESPACE_URI = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2'; - const SCHEMA_LOCATION = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd'; - const TAG_NAME = 'TrackPointExtension'; + public const NAMESPACE_URI = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2'; + public const SCHEMA_LOCATION = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd'; + public const TAG_NAME = 'TrackPointExtension'; - public static function getNamespace(): string { return self::NAMESPACE_URI; } - public static function getSchemaLocation(): string { return self::SCHEMA_LOCATION; } - public static function getTagName(): string { return self::TAG_NAME; } + public static function getNamespace(): string + { + return self::NAMESPACE_URI; + } + public static function getSchemaLocation(): string + { + return self::SCHEMA_LOCATION; + } + public static function getTagName(): string + { + return self::TAG_NAME; + } /** Air temperature in degrees Celsius. */ public ?float $aTemp = null; @@ -58,6 +68,6 @@ public function jsonSerialize(): array 'speed' => $this->speed, 'course' => $this->course, 'bearing' => $this->bearing, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php index 19804c6..61393db 100644 --- a/src/phpGPX/Models/GpxFile.php +++ b/src/phpGPX/Models/GpxFile.php @@ -1,4 +1,5 @@ @@ -41,7 +42,8 @@ class GpxFile implements \JsonSerializable public function __construct( public readonly Config $config = new Config(), - ) {} + ) { + } public function jsonSerialize(): array { @@ -69,7 +71,7 @@ public function jsonSerialize(): array 'metadata' => $this->metadata, 'creator' => $this->creator, 'extensions' => $this->extensions, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } return $result; @@ -88,11 +90,11 @@ public function toJSON(): string */ public function toXML(): \DOMDocument { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $gpx = $document->createElementNS("http://www.topografix.com/GPX/1/1", "gpx"); - $gpx->setAttribute("version", $this->version ?? "1.1"); - $gpx->setAttribute("creator", $this->creator ? $this->creator : phpGPX::getSignature()); + $gpx = $document->createElementNS('http://www.topografix.com/GPX/1/1', 'gpx'); + $gpx->setAttribute('version', $this->version ?? '1.1'); + $gpx->setAttribute('creator', $this->creator ? $this->creator : phpGPX::getSignature()); ExtensionParser::$usedNamespaces = []; ExtensionParser::$registry ??= ExtensionRegistry::default(); @@ -120,14 +122,14 @@ public function toXML(): \DOMDocument // Namespaces $schemaLocationArray = [ 'http://www.topografix.com/GPX/1/1', - 'http://www.topografix.com/GPX/1/1/gpx.xsd' + 'http://www.topografix.com/GPX/1/1/gpx.xsd', ]; foreach (ExtensionParser::$usedNamespaces as $usedNamespace) { $gpx->setAttributeNS( - "http://www.w3.org/2000/xmlns/", - sprintf("xmlns:%s", $usedNamespace['prefix']), - $usedNamespace['namespace'] + 'http://www.w3.org/2000/xmlns/', + sprintf('xmlns:%s', $usedNamespace['prefix']), + $usedNamespace['namespace'], ); $schemaLocationArray[] = $usedNamespace['namespace']; @@ -137,7 +139,7 @@ public function toXML(): \DOMDocument $gpx->setAttributeNS( 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', - implode(" ", $schemaLocationArray) + implode(' ', $schemaLocationArray), ); $document->appendChild($gpx); @@ -164,7 +166,7 @@ public function save(string $path, string $format): void file_put_contents($path, $this->toJSON()); break; default: - throw new \RuntimeException("Unsupported file format!"); + throw new \RuntimeException('Unsupported file format!'); } } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Link.php b/src/phpGPX/Models/Link.php index c3751d0..360568c 100644 --- a/src/phpGPX/Models/Link.php +++ b/src/phpGPX/Models/Link.php @@ -12,7 +12,8 @@ public function __construct( public ?string $href = null, public ?string $text = null, public ?string $type = null, - ) {} + ) { + } public function jsonSerialize(): array { @@ -20,6 +21,6 @@ public function jsonSerialize(): array 'href' => $this->href, 'text' => $this->text, 'type' => $this->type, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Metadata.php b/src/phpGPX/Models/Metadata.php index 57db42b..b776ea1 100644 --- a/src/phpGPX/Models/Metadata.php +++ b/src/phpGPX/Models/Metadata.php @@ -40,6 +40,6 @@ public function jsonSerialize(): array 'keywords' => $this->keywords, 'bounds' => $this->bounds, 'extensions' => $this->extensions, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Person.php b/src/phpGPX/Models/Person.php index be0d4ef..04d99bf 100644 --- a/src/phpGPX/Models/Person.php +++ b/src/phpGPX/Models/Person.php @@ -12,7 +12,8 @@ public function __construct( public ?Email $email = null, /** @var Link[]|null */ public ?array $links = null, - ) {} + ) { + } public function jsonSerialize(): array { @@ -20,6 +21,6 @@ public function jsonSerialize(): array 'name' => $this->name, 'email' => $this->email, 'links' => !empty($this->links) ? $this->links : null, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Point.php b/src/phpGPX/Models/Point.php index 0dd66cf..8654716 100644 --- a/src/phpGPX/Models/Point.php +++ b/src/phpGPX/Models/Point.php @@ -82,7 +82,8 @@ class Point implements \JsonSerializable public function __construct( private readonly PointType $pointType, - ) {} + ) { + } public function getPointType(): PointType { @@ -111,7 +112,7 @@ public function jsonSerialize(): array 'ageofdgpsdata' => $this->ageOfGpsData, 'dgpsid' => $this->dgpsid, 'extensions' => $this->extensions, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); return [ 'type' => 'Feature', @@ -122,4 +123,4 @@ public function jsonSerialize(): array 'properties' => $properties ?: new \stdClass(), ]; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/PointType.php b/src/phpGPX/Models/PointType.php index 2fa4697..882fafe 100644 --- a/src/phpGPX/Models/PointType.php +++ b/src/phpGPX/Models/PointType.php @@ -7,4 +7,4 @@ enum PointType: string case Waypoint = 'wpt'; case Trackpoint = 'trkpt'; case Routepoint = 'rtept'; -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 72d1c18..4691b1a 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -1,4 +1,5 @@ @@ -14,7 +15,6 @@ */ class Route extends Collection { - /** @var Point[] */ public array $points = []; @@ -45,7 +45,7 @@ public function jsonSerialize(): array 'type' => $this->type, 'extensions' => $this->extensions, 'stats' => $this->stats, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); return [ 'type' => 'Feature', @@ -56,4 +56,4 @@ public function jsonSerialize(): array 'properties' => $properties ?: new \stdClass(), ]; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index b83721c..f8680e3 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -26,7 +26,7 @@ public function jsonSerialize(): array $properties = array_filter([ 'extensions' => $this->extensions, 'stats' => $this->stats, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); return [ 'type' => 'Feature', @@ -43,4 +43,4 @@ public function getPoints(): array { return $this->points; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php index a312cd1..75f22c6 100644 --- a/src/phpGPX/Models/Stats.php +++ b/src/phpGPX/Models/Stats.php @@ -1,4 +1,5 @@ @@ -14,7 +15,6 @@ */ class Stats implements \JsonSerializable { - /** * Distance in meters (m) * @var float|null @@ -172,6 +172,6 @@ public function jsonSerialize(): array 'maxHeartRate' => $this->maxHeartRate, 'avgCadence' => $this->averageCadence, 'avgTemperature' => $this->averageTemperature, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); } } diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index bf2b750..fc0d5a1 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -1,4 +1,5 @@ @@ -14,7 +15,6 @@ */ class Track extends Collection { - /** @var Segment[] */ public array $segments = []; @@ -56,7 +56,7 @@ public function jsonSerialize(): array 'type' => $this->type, 'extensions' => $this->extensions, 'stats' => $this->stats, - ], fn($v) => $v !== null); + ], fn ($v) => $v !== null); return [ 'type' => 'Feature', @@ -67,4 +67,4 @@ public function jsonSerialize(): array 'properties' => $properties ?: new \stdClass(), ]; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/AbstractParser.php b/src/phpGPX/Parsers/AbstractParser.php index b8af5b4..c475fbb 100644 --- a/src/phpGPX/Parsers/AbstractParser.php +++ b/src/phpGPX/Parsers/AbstractParser.php @@ -153,4 +153,4 @@ protected static function serializeDelegated(mixed $value, array $attribute, \DO $parentNode->appendChild($parserClass::toXML($value, $document)); } } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/BoundsParser.php b/src/phpGPX/Parsers/BoundsParser.php index a961e6b..869fbf7 100644 --- a/src/phpGPX/Parsers/BoundsParser.php +++ b/src/phpGPX/Parsers/BoundsParser.php @@ -1,4 +1,5 @@ @@ -27,12 +28,12 @@ public static function parse(\SimpleXMLElement $node): ?Bounds return null; } - return new Bounds( - (float) $node['minlat'], - (float) $node['minlon'], - (float) $node['maxlat'], - (float) $node['maxlon'] - ); + return new Bounds( + (float) $node['minlat'], + (float) $node['minlon'], + (float) $node['maxlat'], + (float) $node['maxlon'], + ); } /** diff --git a/src/phpGPX/Parsers/CopyrightParser.php b/src/phpGPX/Parsers/CopyrightParser.php index 0d5a08e..bac6856 100644 --- a/src/phpGPX/Parsers/CopyrightParser.php +++ b/src/phpGPX/Parsers/CopyrightParser.php @@ -1,4 +1,5 @@ diff --git a/src/phpGPX/Parsers/EmailParser.php b/src/phpGPX/Parsers/EmailParser.php index adc74f2..8e14aad 100644 --- a/src/phpGPX/Parsers/EmailParser.php +++ b/src/phpGPX/Parsers/EmailParser.php @@ -1,4 +1,5 @@ diff --git a/src/phpGPX/Parsers/ExtensionParser.php b/src/phpGPX/Parsers/ExtensionParser.php index cce3ab4..8ffa2fd 100644 --- a/src/phpGPX/Parsers/ExtensionParser.php +++ b/src/phpGPX/Parsers/ExtensionParser.php @@ -90,4 +90,4 @@ public static function toXML(Extensions $extensions, \DOMDocument &$document): \ return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/ExtensionRegistry.php b/src/phpGPX/Parsers/ExtensionRegistry.php index 71edcbb..b1cdcf9 100644 --- a/src/phpGPX/Parsers/ExtensionRegistry.php +++ b/src/phpGPX/Parsers/ExtensionRegistry.php @@ -95,4 +95,4 @@ public static function default(): self ->register('http://www.garmin.com/xmlschemas/TrackPointExtension/v2', TrackPointExtensionParser::class, 'gpxtpx') ->register('http://www.garmin.com/xmlschemas/TrackPointExtension/v1', TrackPointExtensionParser::class, 'gpxtpx'); } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php b/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php index 2414dfc..c5155b5 100644 --- a/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php +++ b/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php @@ -44,4 +44,4 @@ public static function parse(\SimpleXMLElement $node): ExtensionInterface; * @return \DOMElement The serialized XML element */ public static function toXML(ExtensionInterface $extension, \DOMDocument &$document, string $prefix): \DOMElement; -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php index 67b579a..13e9e75 100644 --- a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php +++ b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php @@ -1,4 +1,5 @@ @@ -17,36 +18,36 @@ protected static function getAttributeMapper(): array return [ 'atemp' => [ 'name' => 'aTemp', - 'type' => 'float' + 'type' => 'float', ], 'wtemp' => [ 'name' => 'wTemp', - 'type' => 'float' + 'type' => 'float', ], 'depth' => [ 'name' => 'depth', - 'type' => 'float' + 'type' => 'float', ], 'hr' => [ 'name' => 'hr', - 'type' => 'float' + 'type' => 'float', ], 'cad' => [ 'name' => 'cad', - 'type' => 'float' + 'type' => 'float', ], 'speed' => [ 'name' => 'speed', - 'type' => 'float' + 'type' => 'float', ], 'course' => [ 'name' => 'course', - 'type' => 'int' + 'type' => 'int', ], 'bearing' => [ 'name' => 'bearing', - 'type' => 'int' - ] + 'type' => 'int', + ], ]; } @@ -61,13 +62,13 @@ public static function parse(\SimpleXMLElement $node): ExtensionInterface public static function toXML(ExtensionInterface $extension, \DOMDocument &$document, string $prefix = 'gpxtpx'): \DOMElement { - $node = $document->createElement(sprintf("%s:%s", $prefix, $extension::getTagName())); + $node = $document->createElement(sprintf('%s:%s', $prefix, $extension::getTagName())); foreach (self::getAttributeMapper() as $key => $attribute) { if (isset($extension->{$attribute['name']})) { $child = $document->createElement( - sprintf("%s:%s", $prefix, $key), - $extension->{$attribute['name']} + sprintf('%s:%s', $prefix, $key), + $extension->{$attribute['name']}, ); $node->appendChild($child); } @@ -75,4 +76,4 @@ public static function toXML(ExtensionInterface $extension, \DOMDocument &$docum return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/LinkParser.php b/src/phpGPX/Parsers/LinkParser.php index 7e8d7c7..8b63895 100644 --- a/src/phpGPX/Parsers/LinkParser.php +++ b/src/phpGPX/Parsers/LinkParser.php @@ -1,4 +1,5 @@ @@ -53,4 +54,4 @@ public static function toXML(Link $link, \DOMDocument &$document): \DOMElement return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/MetadataParser.php b/src/phpGPX/Parsers/MetadataParser.php index fa00311..56b75a2 100644 --- a/src/phpGPX/Parsers/MetadataParser.php +++ b/src/phpGPX/Parsers/MetadataParser.php @@ -1,4 +1,5 @@ @@ -22,11 +23,11 @@ protected static function getAttributeMapper(): array return [ 'name' => [ 'name' => 'name', - 'type' => 'string' + 'type' => 'string', ], 'desc' => [ 'name' => 'description', - 'type' => 'string' + 'type' => 'string', ], 'author' => [ 'name' => 'author', @@ -45,11 +46,11 @@ protected static function getAttributeMapper(): array ], 'time' => [ 'name' => 'time', - 'type' => 'datetime' + 'type' => 'datetime', ], 'keywords' => [ 'name' => 'keywords', - 'type' => 'string' + 'type' => 'string', ], 'bounds' => [ 'name' => 'bounds', @@ -60,7 +61,7 @@ protected static function getAttributeMapper(): array 'name' => 'extensions', 'type' => 'object', 'parser' => ExtensionParser::class, - ] + ], ]; } @@ -113,4 +114,4 @@ public static function toXML(Metadata $metadata, \DOMDocument &$document): \DOME return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/PersonParser.php b/src/phpGPX/Parsers/PersonParser.php index fc9afb5..75f4530 100644 --- a/src/phpGPX/Parsers/PersonParser.php +++ b/src/phpGPX/Parsers/PersonParser.php @@ -1,4 +1,5 @@ diff --git a/src/phpGPX/Parsers/PointParser.php b/src/phpGPX/Parsers/PointParser.php index 7f308d7..de896ba 100644 --- a/src/phpGPX/Parsers/PointParser.php +++ b/src/phpGPX/Parsers/PointParser.php @@ -13,35 +13,35 @@ protected static function getAttributeMapper(): array return [ 'ele' => [ 'name' => 'elevation', - 'type' => 'float' + 'type' => 'float', ], 'time' => [ 'name' => 'time', - 'type' => 'datetime' + 'type' => 'datetime', ], 'magvar' => [ 'name' => 'magVar', - 'type' => 'float' + 'type' => 'float', ], 'geoidheight' => [ 'name' => 'geoidHeight', - 'type' => 'float' + 'type' => 'float', ], 'name' => [ 'name' => 'name', - 'type' => 'string' + 'type' => 'string', ], 'cmt' => [ 'name' => 'comment', - 'type' => 'string' + 'type' => 'string', ], 'desc' => [ 'name' => 'description', - 'type' => 'string' + 'type' => 'string', ], 'src' => [ 'name' => 'source', - 'type' => 'string' + 'type' => 'string', ], 'link' => [ 'name' => 'links', @@ -50,45 +50,45 @@ protected static function getAttributeMapper(): array ], 'sym' => [ 'name' => 'symbol', - 'type' => 'string' + 'type' => 'string', ], 'type' => [ 'name' => 'type', - 'type' => 'string' + 'type' => 'string', ], 'fix' => [ 'name' => 'fix', - 'type' => 'string' + 'type' => 'string', ], 'sat' => [ 'name' => 'satellitesNumber', - 'type' => 'integer' + 'type' => 'integer', ], 'hdop' => [ 'name' => 'hdop', - 'type' => 'float' + 'type' => 'float', ], 'vdop' => [ 'name' => 'vdop', - 'type' => 'float' + 'type' => 'float', ], 'pdop' => [ 'name' => 'pdop', - 'type' => 'float' + 'type' => 'float', ], 'ageofdgpsdata' => [ 'name' => 'ageOfGpsData', - 'type' => 'float' + 'type' => 'float', ], 'dgpsid' => [ 'name' => 'dgpsid', - 'type' => 'integer' + 'type' => 'integer', ], 'extensions' => [ 'name' => 'extensions', 'type' => 'object', 'parser' => ExtensionParser::class, - ] + ], ]; } @@ -139,4 +139,4 @@ public static function toXML(Point $point, \DOMDocument &$document): \DOMElement return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/RouteParser.php b/src/phpGPX/Parsers/RouteParser.php index a13bc90..fa02f20 100644 --- a/src/phpGPX/Parsers/RouteParser.php +++ b/src/phpGPX/Parsers/RouteParser.php @@ -1,4 +1,5 @@ @@ -21,19 +22,19 @@ protected static function getAttributeMapper(): array return [ 'name' => [ 'name' => 'name', - 'type' => 'string' + 'type' => 'string', ], 'cmt' => [ 'name' => 'comment', - 'type' => 'string' + 'type' => 'string', ], 'desc' => [ 'name' => 'description', - 'type' => 'string' + 'type' => 'string', ], 'src' => [ 'name' => 'source', - 'type' => 'string' + 'type' => 'string', ], 'link' => [ 'name' => 'links', @@ -42,11 +43,11 @@ protected static function getAttributeMapper(): array ], 'number' => [ 'name' => 'number', - 'type' => 'integer' + 'type' => 'integer', ], 'type' => [ 'name' => 'type', - 'type' => 'string' + 'type' => 'string', ], 'extensions' => [ 'name' => 'extensions', @@ -106,4 +107,4 @@ public static function toXML(Route $route, \DOMDocument &$document): \DOMElement return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/SegmentParser.php b/src/phpGPX/Parsers/SegmentParser.php index 2067772..93786c9 100644 --- a/src/phpGPX/Parsers/SegmentParser.php +++ b/src/phpGPX/Parsers/SegmentParser.php @@ -1,4 +1,5 @@ @@ -63,4 +64,4 @@ public static function toXML(Segment $segment, \DOMDocument &$document): \DOMEle return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/TrackParser.php b/src/phpGPX/Parsers/TrackParser.php index 0a455be..2e30952 100644 --- a/src/phpGPX/Parsers/TrackParser.php +++ b/src/phpGPX/Parsers/TrackParser.php @@ -1,4 +1,5 @@ @@ -21,19 +22,19 @@ protected static function getAttributeMapper(): array return [ 'name' => [ 'name' => 'name', - 'type' => 'string' + 'type' => 'string', ], 'cmt' => [ 'name' => 'comment', - 'type' => 'string' + 'type' => 'string', ], 'desc' => [ 'name' => 'description', - 'type' => 'string' + 'type' => 'string', ], 'src' => [ 'name' => 'source', - 'type' => 'string' + 'type' => 'string', ], 'link' => [ 'name' => 'links', @@ -42,11 +43,11 @@ protected static function getAttributeMapper(): array ], 'number' => [ 'name' => 'number', - 'type' => 'integer' + 'type' => 'integer', ], 'type' => [ 'name' => 'type', - 'type' => 'string' + 'type' => 'string', ], 'extensions' => [ 'name' => 'extensions', @@ -106,4 +107,4 @@ public static function toXML(Track $track, \DOMDocument &$document): \DOMElement return $node; } -} \ No newline at end of file +} diff --git a/src/phpGPX/Parsers/WaypointParser.php b/src/phpGPX/Parsers/WaypointParser.php index f1ea104..7f9364e 100644 --- a/src/phpGPX/Parsers/WaypointParser.php +++ b/src/phpGPX/Parsers/WaypointParser.php @@ -1,4 +1,5 @@ @@ -12,7 +13,6 @@ */ abstract class WaypointParser { - /** * @param \SimpleXMLElement $nodes - a non empty list of wpt elements * @return array diff --git a/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php index d1e9eca..d59f56f 100644 --- a/src/phpGPX/phpGPX.php +++ b/src/phpGPX/phpGPX.php @@ -1,4 +1,5 @@ @@ -21,12 +22,12 @@ */ class phpGPX { - const JSON_FORMAT = 'json'; - const XML_FORMAT = 'xml'; - const GEOJSON_FORMAT = 'geojson'; + public const JSON_FORMAT = 'json'; + public const XML_FORMAT = 'xml'; + public const GEOJSON_FORMAT = 'geojson'; - const PACKAGE_NAME = 'phpGPX'; - const VERSION = '2.0.0-beta.1'; + public const PACKAGE_NAME = 'phpGPX'; + public const VERSION = '2.0.0-beta.1'; public readonly Config $config; @@ -108,6 +109,6 @@ public function parse(string $xml): GpxFile */ public static function getSignature(): string { - return sprintf("%s/%s", self::PACKAGE_NAME, self::VERSION); + return sprintf('%s/%s', self::PACKAGE_NAME, self::VERSION); } -} \ No newline at end of file +} diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php index bcfe9d5..c676441 100644 --- a/tests/Integration/GeoJsonOutputTest.php +++ b/tests/Integration/GeoJsonOutputTest.php @@ -165,4 +165,4 @@ public function testToJsonOutput(): void $this->assertEquals('FeatureCollection', $decoded['type']); $this->assertArrayHasKey('features', $decoded); } -} \ No newline at end of file +} diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php index 12a5c54..57b3ed2 100644 --- a/tests/Integration/GpxFileLoadTest.php +++ b/tests/Integration/GpxFileLoadTest.php @@ -163,4 +163,4 @@ public function testParseFromString(): void $this->assertCount(2, $gpxFile->routes); $this->assertEquals("Patrick's Route", $gpxFile->routes[0]->name); } -} \ No newline at end of file +} diff --git a/tests/Integration/XmlRoundTripTest.php b/tests/Integration/XmlRoundTripTest.php index 9c9398f..d166f25 100644 --- a/tests/Integration/XmlRoundTripTest.php +++ b/tests/Integration/XmlRoundTripTest.php @@ -34,7 +34,7 @@ public function testRoundTripTimezero(): void $this->assertEqualsWithDelta( $original->waypoints[$i]->latitude, $reloaded->waypoints[$i]->latitude, - 0.0001 + 0.0001, ); $this->assertEquals($original->waypoints[$i]->name, $reloaded->waypoints[$i]->name); } @@ -44,13 +44,13 @@ public function testRoundTripTimezero(): void $this->assertEquals($original->tracks[$t]->name, $reloaded->tracks[$t]->name); $this->assertCount( count($original->tracks[$t]->segments), - $reloaded->tracks[$t]->segments + $reloaded->tracks[$t]->segments, ); for ($s = 0; $s < count($original->tracks[$t]->segments); $s++) { $this->assertCount( count($original->tracks[$t]->segments[$s]->points), - $reloaded->tracks[$t]->segments[$s]->points + $reloaded->tracks[$t]->segments[$s]->points, ); } } @@ -68,7 +68,7 @@ public function testRoundTripRoute(): void $this->assertEquals($original->routes[$r]->name, $reloaded->routes[$r]->name); $this->assertCount( count($original->routes[$r]->points), - $reloaded->routes[$r]->points + $reloaded->routes[$r]->points, ); for ($p = 0; $p < count($original->routes[$r]->points); $p++) { @@ -101,7 +101,7 @@ public function testRoundTripGpsTrack(): void $this->assertEqualsWithDelta( $origSeg->points[$i]->elevation, $reloadedSeg->points[$i]->elevation, - 0.01 + 0.01, ); } } @@ -129,7 +129,7 @@ public function testRoundTripMinimalWithExtensions(): void $this->assertEqualsWithDelta( $origPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr, $reloadedPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr, - 0.1 + 0.1, ); } @@ -149,7 +149,7 @@ public function testRoundTripStatsConsistency(): void $this->assertEqualsWithDelta( $origStats->cumulativeElevationGain, $reloadedStats->cumulativeElevationGain, - 0.01 + 0.01, ); } -} \ No newline at end of file +} diff --git a/tests/Unit/Analysis/BoundsAnalyzerTest.php b/tests/Unit/Analysis/BoundsAnalyzerTest.php index 5944339..6aefb32 100644 --- a/tests/Unit/Analysis/BoundsAnalyzerTest.php +++ b/tests/Unit/Analysis/BoundsAnalyzerTest.php @@ -9,7 +9,6 @@ use phpGPX\Models\PointType; use phpGPX\Models\Route; use phpGPX\Models\Segment; -use phpGPX\Models\Stats; use phpGPX\Models\Track; use PHPUnit\Framework\TestCase; @@ -164,4 +163,4 @@ public function testMetadataBoundsIncludesWaypoints(): void $this->assertEqualsWithDelta(17.0, $bounds->minLongitude, 0.001); $this->assertEqualsWithDelta(20.0, $bounds->maxLongitude, 0.001); } -} \ No newline at end of file +} diff --git a/tests/Unit/Analysis/EngineTest.php b/tests/Unit/Analysis/EngineTest.php index 653a90f..22facc8 100644 --- a/tests/Unit/Analysis/EngineTest.php +++ b/tests/Unit/Analysis/EngineTest.php @@ -4,12 +4,7 @@ use phpGPX\Analysis\AbstractPointAnalyzer; use phpGPX\Analysis\BoundsAnalyzer; -use phpGPX\Analysis\DistanceAnalyzer; -use phpGPX\Analysis\ElevationAnalyzer; -use phpGPX\Analysis\MovementAnalyzer; -use phpGPX\Analysis\PointAnalyzerInterface; use phpGPX\Analysis\Engine; -use phpGPX\Analysis\TrackPointExtensionAnalyzer; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; use phpGPX\Models\PointType; @@ -25,7 +20,7 @@ private function makePoint( float $lat, float $lon, ?float $ele = null, - ?string $time = null + ?string $time = null, ): Point { $p = new Point(PointType::Trackpoint); $p->latitude = $lat; @@ -39,18 +34,40 @@ public function testAnalyzersCalledInOrder(): void { $order = []; - $a1 = new class($order) extends AbstractPointAnalyzer { - public function __construct(private array &$order) {} - public function begin(): void { $this->order[] = 'a1:begin'; } - public function visit(Point $current, ?Point $previous): void { $this->order[] = 'a1:visit'; } - public function end(Stats $stats): void { $this->order[] = 'a1:end'; } + $a1 = new class ($order) extends AbstractPointAnalyzer { + public function __construct(private array &$order) + { + } + public function begin(): void + { + $this->order[] = 'a1:begin'; + } + public function visit(Point $current, ?Point $previous): void + { + $this->order[] = 'a1:visit'; + } + public function end(Stats $stats): void + { + $this->order[] = 'a1:end'; + } }; - $a2 = new class($order) extends AbstractPointAnalyzer { - public function __construct(private array &$order) {} - public function begin(): void { $this->order[] = 'a2:begin'; } - public function visit(Point $current, ?Point $previous): void { $this->order[] = 'a2:visit'; } - public function end(Stats $stats): void { $this->order[] = 'a2:end'; } + $a2 = new class ($order) extends AbstractPointAnalyzer { + public function __construct(private array &$order) + { + } + public function begin(): void + { + $this->order[] = 'a2:begin'; + } + public function visit(Point $current, ?Point $previous): void + { + $this->order[] = 'a2:visit'; + } + public function end(Stats $stats): void + { + $this->order[] = 'a2:end'; + } }; $engine = (new Engine())->addAnalyzer($a1)->addAnalyzer($a2); @@ -321,4 +338,4 @@ public function testDefaultFactoryCustomParameters(): void // Only the 17m gain (103→120) counts $this->assertEqualsWithDelta(20.0, $stats->cumulativeElevationGain, 0.01); } -} \ No newline at end of file +} diff --git a/tests/Unit/Analysis/MovementAnalyzerTest.php b/tests/Unit/Analysis/MovementAnalyzerTest.php index 3923d6e..43a7e6b 100644 --- a/tests/Unit/Analysis/MovementAnalyzerTest.php +++ b/tests/Unit/Analysis/MovementAnalyzerTest.php @@ -3,8 +3,8 @@ namespace phpGPX\Tests\Unit\Analysis; use phpGPX\Analysis\DistanceAnalyzer; -use phpGPX\Analysis\MovementAnalyzer; use phpGPX\Analysis\Engine; +use phpGPX\Analysis\MovementAnalyzer; use phpGPX\Models\GpxFile; use phpGPX\Models\Point; use phpGPX\Models\PointType; @@ -176,4 +176,4 @@ public function testSinglePointReturnsNull(): void $this->assertNull($result->tracks[0]->segments[0]->stats->movingDuration); } -} \ No newline at end of file +} diff --git a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php index e03fdb4..5f12f6c 100644 --- a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php +++ b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php @@ -28,7 +28,7 @@ private function makePointWithExtension( float $lon, ?float $hr = null, ?float $cad = null, - ?float $aTemp = null + ?float $aTemp = null, ): Point { $p = new Point(PointType::Trackpoint); $p->latitude = $lat; @@ -172,4 +172,4 @@ public function testRouteExtensionStats(): void $this->assertEqualsWithDelta(140.0, $result->routes[0]->stats->averageHeartRate, 0.001); } -} \ No newline at end of file +} diff --git a/tests/Unit/Helpers/DateTimeHelperTest.php b/tests/Unit/Helpers/DateTimeHelperTest.php index 72a416c..f2a8cf8 100644 --- a/tests/Unit/Helpers/DateTimeHelperTest.php +++ b/tests/Unit/Helpers/DateTimeHelperTest.php @@ -9,34 +9,34 @@ class DateTimeHelperTest extends TestCase { public function testFormatDateTime(): void { - $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); + $datetime = new \DateTime('2017-08-12T20:16:29+00:00'); $this->assertEquals( - $datetime->format("Y-m-d H:i:s"), - DateTimeHelper::formatDateTime($datetime, "Y-m-d H:i:s") + $datetime->format('Y-m-d H:i:s'), + DateTimeHelper::formatDateTime($datetime, 'Y-m-d H:i:s'), ); - $this->assertNull(DateTimeHelper::formatDateTime(null), "NULL input"); - $this->assertNull(DateTimeHelper::formatDateTime(""), "Empty string input"); + $this->assertNull(DateTimeHelper::formatDateTime(null), 'NULL input'); + $this->assertNull(DateTimeHelper::formatDateTime(''), 'Empty string input'); - $datetime = new \DateTime("2017-08-12T20:16:29+00:00"); + $datetime = new \DateTime('2017-08-12T20:16:29+00:00'); $this->assertEquals( - "2017-08-12 21:16:29", - DateTimeHelper::formatDateTime($datetime, "Y-m-d H:i:s", '+01:00') + '2017-08-12 21:16:29', + DateTimeHelper::formatDateTime($datetime, 'Y-m-d H:i:s', '+01:00'), ); } public function testParseDateTime(): void { $this->assertEquals( - new \DateTime("2017-08-12T20:16:29+00:00"), - DateTimeHelper::parseDateTime("2017-08-12T20:16:29+00:00") + new \DateTime('2017-08-12T20:16:29+00:00'), + DateTimeHelper::parseDateTime('2017-08-12T20:16:29+00:00'), ); } public function testParseDateTimeInvalidInput(): void { - $this->expectException("Exception"); - DateTimeHelper::parseDateTime("Invalid exception"); + $this->expectException('Exception'); + DateTimeHelper::parseDateTime('Invalid exception'); } -} \ No newline at end of file +} diff --git a/tests/Unit/Helpers/DistanceCalculatorTest.php b/tests/Unit/Helpers/DistanceCalculatorTest.php index 1439739..2fee611 100644 --- a/tests/Unit/Helpers/DistanceCalculatorTest.php +++ b/tests/Unit/Helpers/DistanceCalculatorTest.php @@ -119,4 +119,4 @@ public function testSamePointRepeatedZeroDistance(): void $calc = new DistanceCalculator([$p1, $p2, $p3]); $this->assertEqualsWithDelta(0.0, $calc->getRawDistance(), 0.001); } -} \ No newline at end of file +} diff --git a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php index 917c69f..8a34694 100644 --- a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php +++ b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php @@ -184,4 +184,4 @@ public function testSmoothingSpikesThreshold(): void $this->assertEqualsWithDelta(5.0, $gain, 0.001); $this->assertEqualsWithDelta(0.0, $loss, 0.001); } -} \ No newline at end of file +} diff --git a/tests/Unit/Helpers/GeoHelperTest.php b/tests/Unit/Helpers/GeoHelperTest.php index 4127a5a..43d9457 100644 --- a/tests/Unit/Helpers/GeoHelperTest.php +++ b/tests/Unit/Helpers/GeoHelperTest.php @@ -30,7 +30,7 @@ public function testGetDistance(): void 856.97, GeoHelper::getRawDistance($point1, $point2), 1, - "Invalid distance between two points!" + 'Invalid distance between two points!', ); } @@ -53,14 +53,14 @@ public function testRealDistance(): void 856.97, GeoHelper::getRawDistance($point1, $point2), 1, - "Invalid distance between two points!" + 'Invalid distance between two points!', ); $this->assertEqualsWithDelta( 862, GeoHelper::getRealDistance($point1, $point2), 1, - "Invalid real distance between two points!" + 'Invalid real distance between two points!', ); } @@ -92,4 +92,4 @@ public function testRealDistanceWithNullElevation(): void $realDist = GeoHelper::getRealDistance($point1, $point2); $this->assertEqualsWithDelta($rawDist, $realDist, 0.001); } -} \ No newline at end of file +} diff --git a/tests/Unit/Helpers/SerializationHelperTest.php b/tests/Unit/Helpers/SerializationHelperTest.php index 314804b..f398b98 100644 --- a/tests/Unit/Helpers/SerializationHelperTest.php +++ b/tests/Unit/Helpers/SerializationHelperTest.php @@ -24,4 +24,4 @@ public function testPositionWithNullElevation(): void $pos = SerializationHelper::position(9.860, 54.932, null); $this->assertEquals([9.860, 54.932], $pos); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/BoundsTest.php b/tests/Unit/Models/BoundsTest.php index a06421a..8f9fed1 100644 --- a/tests/Unit/Models/BoundsTest.php +++ b/tests/Unit/Models/BoundsTest.php @@ -16,7 +16,7 @@ protected function setUp(): void 49.072489, 18.814543, 49.090543, - 18.886939 + 18.886939, ); } @@ -53,4 +53,4 @@ public function testParseInvalidNode(): void $this->assertNull($bounds); } -} \ No newline at end of file +} diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php index 1296beb..938ee5a 100644 --- a/tests/Unit/Models/StatsCalculationTest.php +++ b/tests/Unit/Models/StatsCalculationTest.php @@ -33,7 +33,7 @@ private function makePoint( float $lat, float $lon, ?float $ele = null, - ?string $time = null + ?string $time = null, ): Point { $p = new Point(PointType::Trackpoint); $p->latitude = $lat; @@ -47,7 +47,7 @@ private function makeRoutePoint( float $lat, float $lon, ?float $ele = null, - ?string $time = null + ?string $time = null, ): Point { $p = new Point(PointType::Routepoint); $p->latitude = $lat; @@ -369,4 +369,4 @@ public function testStatsJsonSerialize(): void $this->assertEquals(200.0, $json['maxAltitude']); $this->assertEquals(400.0, $json['duration']); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/BoundsParserTest.php b/tests/Unit/Parsers/BoundsParserTest.php index 4b3af32..31115b2 100644 --- a/tests/Unit/Parsers/BoundsParserTest.php +++ b/tests/Unit/Parsers/BoundsParserTest.php @@ -19,7 +19,7 @@ protected function setUp(): void 49.072489, 18.814543, 49.090543, - 18.886939 + 18.886939, ); $this->file = simplexml_load_file(self::FIXTURES_DIR . '/bounds.xml'); @@ -42,13 +42,13 @@ public function testParse(): void public function testToXML(): void { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $root = $document->createElement("document"); + $root = $document->createElement('document'); $root->appendChild(BoundsParser::toXML($this->bounds, $document)); $document->appendChild($root); $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML()); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/CopyrightParserTest.php b/tests/Unit/Parsers/CopyrightParserTest.php index 08936aa..1f66e39 100644 --- a/tests/Unit/Parsers/CopyrightParserTest.php +++ b/tests/Unit/Parsers/CopyrightParserTest.php @@ -16,8 +16,8 @@ class CopyrightParserTest extends TestCase protected function setUp(): void { $this->copyright = new Copyright(); - $this->copyright->author = "Jakub Dubec"; - $this->copyright->license = "https://github.com/Sibyx/phpGPX/blob/master/LICENSE"; + $this->copyright->author = 'Jakub Dubec'; + $this->copyright->license = 'https://github.com/Sibyx/phpGPX/blob/master/LICENSE'; $this->copyright->year = '2017'; $this->file = simplexml_load_file(self::FIXTURES_DIR . '/copyright.xml'); @@ -39,9 +39,9 @@ public function testParse(): void public function testToXML(): void { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $root = $document->createElement("document"); + $root = $document->createElement('document'); $root->appendChild(CopyrightParser::toXML($this->copyright, $document)); $document->appendChild($root); @@ -52,7 +52,8 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/copyright.json', json_encode($this->copyright->jsonSerialize()) + self::FIXTURES_DIR . '/copyright.json', + json_encode($this->copyright->jsonSerialize()), ); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/EmailParserTest.php b/tests/Unit/Parsers/EmailParserTest.php index c2f7b0e..57edb9a 100644 --- a/tests/Unit/Parsers/EmailParserTest.php +++ b/tests/Unit/Parsers/EmailParserTest.php @@ -16,8 +16,8 @@ class EmailParserTest extends TestCase protected function setUp(): void { $this->email = new Email(); - $this->email->id = "jakub.dubec"; - $this->email->domain = "gmail.com"; + $this->email->id = 'jakub.dubec'; + $this->email->domain = 'gmail.com'; $this->file = simplexml_load_file(self::FIXTURES_DIR . '/email.xml'); } @@ -37,9 +37,9 @@ public function testParse(): void public function testToXML(): void { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $root = $document->createElement("document"); + $root = $document->createElement('document'); $root->appendChild(EmailParser::toXML($this->email, $document)); $document->appendChild($root); @@ -50,7 +50,8 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/email.json', json_encode($this->email->jsonSerialize()) + self::FIXTURES_DIR . '/email.json', + json_encode($this->email->jsonSerialize()), ); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/ExtensionParserTest.php b/tests/Unit/Parsers/ExtensionParserTest.php index c41c9dc..217f23e 100644 --- a/tests/Unit/Parsers/ExtensionParserTest.php +++ b/tests/Unit/Parsers/ExtensionParserTest.php @@ -42,15 +42,16 @@ public function testParse(): void $this->assertEquals($expected->jsonSerialize(), $parsed->jsonSerialize()); $this->assertJsonStringEqualsJsonString( - json_encode($this->extensions), json_encode($extensions) + json_encode($this->extensions), + json_encode($extensions), ); } public function testToXML(): void { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $root = $document->createElement("document"); + $root = $document->createElement('document'); $root->appendChild(ExtensionParser::toXML($this->extensions, $document)); $attributes = [ @@ -75,7 +76,8 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/extension.json', json_encode($this->extensions->jsonSerialize()) + self::FIXTURES_DIR . '/extension.json', + json_encode($this->extensions->jsonSerialize()), ); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/ExtensionRegistryTest.php b/tests/Unit/Parsers/ExtensionRegistryTest.php index 762d03f..eee3142 100644 --- a/tests/Unit/Parsers/ExtensionRegistryTest.php +++ b/tests/Unit/Parsers/ExtensionRegistryTest.php @@ -19,7 +19,7 @@ public function testDefaultRegistersTrackPointExtension(): void $this->assertTrue($registry->has(self::GARMIN_TPE_V1)); $this->assertSame( TrackPointExtensionParser::class, - $registry->getParserClass(self::GARMIN_TPE_V2) + $registry->getParserClass(self::GARMIN_TPE_V2), ); } @@ -96,4 +96,4 @@ public function testDefaultPrefixIsExt(): void $this->assertSame('ext', $registry->getPrefix('http://example.com/ext')); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/LinkParserTest.php b/tests/Unit/Parsers/LinkParserTest.php index f8c9326..62d5f95 100644 --- a/tests/Unit/Parsers/LinkParserTest.php +++ b/tests/Unit/Parsers/LinkParserTest.php @@ -16,9 +16,9 @@ class LinkParserTest extends TestCase protected function setUp(): void { $this->link = new Link(); - $this->link->href = "https://jakubdubec.me"; - $this->link->text = "Portfolio"; - $this->link->type = "text/html"; + $this->link->href = 'https://jakubdubec.me'; + $this->link->text = 'Portfolio'; + $this->link->type = 'text/html'; $this->file = simplexml_load_file(self::FIXTURES_DIR . '/link.xml'); } @@ -37,9 +37,9 @@ public function testParse(): void public function testToXML(): void { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $root = $document->createElement("document"); + $root = $document->createElement('document'); $root->appendChild(LinkParser::toXML($this->link, $document)); $document->appendChild($root); @@ -50,7 +50,8 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/link.json', json_encode($this->link->jsonSerialize()) + self::FIXTURES_DIR . '/link.json', + json_encode($this->link->jsonSerialize()), ); } -} \ No newline at end of file +} diff --git a/tests/Unit/Parsers/PersonParserTest.php b/tests/Unit/Parsers/PersonParserTest.php index b0f788f..f7d1c6f 100644 --- a/tests/Unit/Parsers/PersonParserTest.php +++ b/tests/Unit/Parsers/PersonParserTest.php @@ -20,17 +20,17 @@ class PersonParserTest extends TestCase protected function setUp(): void { $this->person = new Person(); - $this->person->name = "Jakub Dubec"; + $this->person->name = 'Jakub Dubec'; $email = new Email(); - $email->id = "jakub.dubec"; - $email->domain = "gmail.com"; + $email->id = 'jakub.dubec'; + $email->domain = 'gmail.com'; $this->person->email = $email; $link = new Link(); - $link->href = "https://jakubdubec.me"; - $link->text = "Portfolio"; - $link->type = "text/html"; + $link->href = 'https://jakubdubec.me'; + $link->text = 'Portfolio'; + $link->type = 'text/html'; $this->person->links[] = $link; $this->file = simplexml_load_file(self::FIXTURES_DIR . '/person.xml'); @@ -66,16 +66,16 @@ public function testEmptyLinks(): void $gpx_file->metadata = new Metadata(); $gpx_file->metadata->author = new Person(); - $gpx_file->metadata->author->name = "Arthur Dent"; + $gpx_file->metadata->author->name = 'Arthur Dent'; $this->assertNotNull($gpx_file->toXML()->saveXML()); } public function testToXML(): void { - $document = new \DOMDocument("1.0", 'UTF-8'); + $document = new \DOMDocument('1.0', 'UTF-8'); - $root = $document->createElement("document"); + $root = $document->createElement('document'); $root->appendChild(PersonParser::toXML($this->person, $document)); $document->appendChild($root); @@ -86,7 +86,8 @@ public function testToXML(): void public function testToJSON(): void { $this->assertJsonStringEqualsJsonFile( - self::FIXTURES_DIR . '/person.json', json_encode($this->person->jsonSerialize()) + self::FIXTURES_DIR . '/person.json', + json_encode($this->person->jsonSerialize()), ); } -} \ No newline at end of file +} From 54c8710711517b317e2fc61248c56d344b863e44 Mon Sep 17 00:00:00 2001 From: Jakub Dubec Date: Mon, 9 Mar 2026 15:43:04 +0100 Subject: [PATCH 31/31] =?UTF-8?q?Upgrade=20Codecov=20actions=20to=20v5=20a?= =?UTF-8?q?nd=20simplify=20configuration=20=F0=9F=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8595a71..fd280ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,36 +54,34 @@ jobs: - name: Upload unit coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage-unit.xml flags: unit - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload integration coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage-integration.xml flags: integration - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload unit test results to Codecov if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v5 with: files: junit-unit.xml flags: unit - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload integration test results to Codecov if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v5 with: files: junit-integration.xml flags: integration - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + token: ${{ secrets.CODECOV_TOKEN }}