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/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fd280ae
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,87 @@
+name: CI
+
+on:
+ push:
+ branches: [master, develop]
+ pull_request:
+ branches: [master, develop]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version: ['8.1', '8.2', '8.3', '8.4', '8.5']
+ steps:
+ - uses: actions/checkout@v6
+
+ - 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 unit tests with coverage
+ 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
+ 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@v5
+ with:
+ files: coverage-unit.xml
+ flags: unit
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Upload integration coverage to Codecov
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@v5
+ with:
+ files: coverage-integration.xml
+ flags: integration
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Upload unit test results to Codecov
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@v5
+ with:
+ files: junit-unit.xml
+ flags: unit
+ report_type: test_results
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Upload integration test results to Codecov
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@v5
+ with:
+ files: junit-integration.xml
+ flags: integration
+ report_type: test_results
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..2aab6c9
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,149 @@
+name: Docs
+
+on:
+ push:
+ branches: [develop]
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ - '.github/workflows/docs.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+
+ - name: Install mkdocs-material
+ run: pip install mkdocs-material plantuml-markdown
+
+ - 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/
+
+ # 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/ 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
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
deleted file mode 100644
index 72f54b6..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: ['7.3', '8.0', '8.1']
-
- 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 bd38f2c..185d5c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,9 +2,12 @@ composer.phar
composer.lock
/vendor/
/.idea/
-/phpdocs/
-/bin/
.DS_Store?
*.DS_Store
/.php_cs.cache
/.phpunit.result.cache
+/.phpunit.cache
+coverage.xml
+/site/
+/.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 2fcbf78..77aaeb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
# Changelog
+## 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
Changed minimal PHP version to `^7.1` in `composer.json`. Library still should work with PHP5.5+, if you have troubles
diff --git a/README.md b/README.md
index c95a293..9a6aee6 100644
--- a/README.md
+++ b/README.md
@@ -3,282 +3,171 @@
[](https://codeclimate.com/github/Sibyx/phpGPX)
[](https://packagist.org/packages/sibyx/phpgpx)
[](https://packagist.org/packages/sibyx/phpgpx)
-[](https://gitter.im/phpGPX/Lobby)
+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).
-[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.
-
-Repository branches:
-
-- `master`: latest stable version
-- `develop`: works on `2.x`
-
-## 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)
-- Min / max coordinates ([lat,lng])
-- (Smoothed) Elevation gain / loss (m)
-- Start / end (DateTime object)
-- Start / end coordinates ([lat,lng])
-- Duration (seconds)
+- 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/). There is no stable release yet, so
-please use release candidates.
-
```
-composer require sibyx/phpgpx:1.3.0
+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
- $track->stats->toArray();
-
- foreach ($track->segments as $segment)
- {
- // Statistics for segment of track
- $segment->stats->toArray();
+
+foreach ($file->tracks as $track) {
+ echo "Distance: " . round($track->stats->distance) . " m\n";
+ echo "Duration: " . gmdate("H:i:s", $track->stats->duration) . "\n";
+
+ foreach ($track->segments as $segment) {
+ echo " Segment distance: " . round($segment->stats->distance) . " m\n";
}
}
```
-### Writing to file
-```php
-load('example.gpx');
+### Saving files
-// XML
+```php
$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();
+Output formatting is configured via the `Config` value object. Stats computation is configured via analyzer constructor arguments.
-// Time attribute is always \DateTime object!
-$gpx_file->metadata->time = new \DateTime();
+```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');
+```
-// Description of GPX file
-$gpx_file->metadata->description = "My pretty awesome GPX file, created using phpGPX library!";
+### Custom engine
-// Adding link created before to links array of metadata
-// Metadata of GPX file can contain more than one link
-$gpx_file->metadata->links[] = $link;
+For fine-grained control, build the engine manually with only the analyzers you need:
-// Creating track
-$track = new Track();
+```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);
+```
-// Name of track
-$track->name = "Some random points in logical order. Input array should be already ordered!";
+### Stats reference
-// Type of data stored in track
-$track->type = 'RUN';
+The engine provides the following stats through its analyzers:
-// Source of GPS coordinates
-$track->source = "MySpecificGarminDevice";
+| 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` |
-// Creating Track segment
-$segment = new Segment();
+### Custom extensions
+Built-in support for Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd) (v1 + v2). Register your own via `ExtensionInterface` + `ExtensionParserInterface`:
-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'];
+```php
+$gpx = new phpGPX();
+$gpx->registerExtension('http://example.com/ext/v1', MyExtensionParser::class, 'myext');
+```
- $segment->points[] = $point;
-}
+### Creating a file from scratch
-// Add segment to segment array of track
-$track->segments[] = $segment;
+```php
+recalculateStats();
+$gpx_file = new GpxFile();
-// Add track to file
-$gpx_file->tracks[] = $track;
+$track = new Track();
+$track->name = "Morning run";
+$track->type = 'RUN';
-// GPX output
-$gpx_file->save('CreatingFileFromScratchExample.gpx', \phpGPX\phpGPX::XML_FORMAT);
+$segment = new Segment();
-// Serialized data as JSON
-$gpx_file->save('CreatingFileFromScratchExample.json', \phpGPX\phpGPX::JSON_FORMAT);
+$point = new Point(PointType::Trackpoint);
+$point->latitude = 54.9328621088893;
+$point->longitude = 9.860624216140083;
+$point->elevation = 0;
+$point->time = new \DateTime("2024-01-15T07:00:00Z");
-// Direct GPX output to browser
+// Add extension data
+$point->extensions = new Extensions();
+$ext = new TrackPointExtension();
+$ext->hr = 145.0;
+$point->extensions->set($ext);
-header("Content-Type: application/gpx+xml");
-header("Content-Disposition: attachment; filename=CreatingFileFromScratchExample.gpx");
+$segment->points[] = $point;
+$track->segments[] = $segment;
+$gpx_file->tracks[] = $track;
-echo $gpx_file->toXML()->saveXML();
-exit();
+$gpx_file->save('output.gpx', \phpGPX\phpGPX::XML_FORMAT);
+$gpx_file->save('output.json', \phpGPX\phpGPX::JSON_FORMAT);
```
-Currently, supported output formats:
-
- - XML
- - JSON
-
-## Configuration
+## Contributing
-Use the static constants in phpGPX to modify behaviour.
+Contributions and feedback are welcome! Please check [the issues](https://github.com/Sibyx/phpGPX/issues).
-```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;
-```
+Repository branches:
+- `master` — latest stable release
+- `develop` — 2.x development
-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 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/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
diff --git a/composer.json b/composer.json
index 5bdf28b..e23ae06 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,17 @@
{
"name": "sibyx/phpgpx",
"type": "library",
- "version": "1.3.0",
- "description": "A simple PHP library for GPX import/export",
+ "version": "2.0.0-beta.1",
+ "description": "A simple PHP library for GPX manipulation",
"minimum-stability": "stable",
+ "keywords": [
+ "gpx",
+ "geospacial",
+ "parser",
+ "geo"
+ ],
+ "homepage": "https://sibyx.github.io/phpGPX/",
+ "readme": "README.md",
"license": "MIT",
"authors": [
{
@@ -13,20 +21,27 @@
}
],
"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.5 || ^11.0 || ^12.0",
+ "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\\": "tests" }
+ },
+ "scripts": {
+ "test": "phpunit",
+ "test:unit": "phpunit --testsuite unit",
+ "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/_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/development/contributing.md b/docs/development/contributing.md
new file mode 100644
index 0000000..a5feb48
--- /dev/null
+++ b/docs/development/contributing.md
@@ -0,0 +1,46 @@
+# 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 and parser test fixture files
+- `docs/` - Documentation (mkdocs-material)
+
+## 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 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/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/development/stats-architecture.md b/docs/development/stats-architecture.md
new file mode 100644
index 0000000..8fc339a
--- /dev/null
+++ b/docs/development/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/development/testing.md b/docs/development/testing.md
new file mode 100644
index 0000000..aa17c41
--- /dev/null
+++ b/docs/development/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/getting-started/installation.md b/docs/getting-started/installation.md
new file mode 100644
index 0000000..f89cb92
--- /dev/null
+++ b/docs/getting-started/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/getting-started/quick-start.md b/docs/getting-started/quick-start.md
new file mode 100644
index 0000000..4796847
--- /dev/null
+++ b/docs/getting-started/quick-start.md
@@ -0,0 +1,68 @@
+# Quick Start
+
+## Loading a GPX file
+
+```php
+use phpGPX\phpGPX;
+use phpGPX\Analysis\Engine;
+
+$gpx = new phpGPX(engine: Engine::default());
+
+$file = $gpx->load('path/to/file.gpx');
+```
+
+You can also parse GPX data from a string:
+
+```php
+$gpx = new phpGPX(engine: Engine::default());
+
+$xml = file_get_contents('path/to/file.gpx');
+$file = $gpx->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
+// $track->stats is populated because engine was provided above
+foreach ($file->tracks as $track) {
+ echo $track->name . "\n";
+ echo "Distance: " . round($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;
+
+$gpx = new phpGPX();
+$file = $gpx->load('input.gpx');
+
+// Save as GPX XML
+$file->save('output.gpx', phpGPX::XML_FORMAT);
+
+// Save as GeoJSON
+$file->save('output.geojson', phpGPX::JSON_FORMAT);
+```
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 7e7626c..b9f119c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,241 +1,35 @@
-Simple library written in PHP for reading and creating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format).
+---
+title: phpGPX Documentation
+---
-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.
+# phpGPX
-Contribution and feedback is welcome! Please check the issues for TODO. I will be happy every feature or pull request.
+A PHP library for reading, creating, and manipulating [GPX files](https://en.wikipedia.org/wiki/GPS_Exchange_Format).
## Features
- - Full support of [official specification](http://www.topografix.com/GPX/1/1/).
- - Statistics calculation.
- - Extensions.
- - JSON & XML & PHP Array output.
+- Full support of [GPX 1.1 specification](http://www.topografix.com/GPX/1/1/)
+- 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
-### Supported Extensions
- - Garmin [TrackPointExtension](https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd): http://www.garmin.com/xmlschemas/TrackPointExtension/v1
-
-### Stats calculation
+## Quick Example
- - (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!";
+$gpx = new phpGPX(engine: Engine::default());
+$file = $gpx->load('track.gpx');
-// 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;
+foreach ($file->tracks as $track) {
+ echo $track->stats->distance . " meters\n";
+ echo $track->stats->cumulativeElevationGain . " meters gained\n";
}
-
-// 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
+## Requirements
-This project is licensed under the terms of the MIT license.
+- PHP >= 8.1
+- `ext-simplexml`
+- `ext-dom`
\ No newline at end of file
diff --git a/docs/output-formats/json.md b/docs/output-formats/json.md
new file mode 100644
index 0000000..e07f011
--- /dev/null
+++ b/docs/output-formats/json.md
@@ -0,0 +1,94 @@
+# JSON (GeoJSON)
+
+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
+$geoJsonString = $file->toJSON();
+```
+
+## 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 }
+ }
+ }
+ ],
+ "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
+
+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
+use phpGPX\Config;
+
+$gpx = new phpGPX(new Config(prettyPrint: false));
+$file = $gpx->load('track.gpx');
+echo $file->toJSON(); // compact, single-line JSON
+```
+
+## 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/output-formats/xml.md b/docs/output-formats/xml.md
new file mode 100644
index 0000000..f02eb6d
--- /dev/null
+++ b/docs/output-formats/xml.md
@@ -0,0 +1,47 @@
+# 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. Disable it via Config:
+
+```php
+use phpGPX\Config;
+
+$gpx = new phpGPX(new Config(prettyPrint: false));
+$file = $gpx->load('input.gpx');
+$file->save('compact.gpx', phpGPX::XML_FORMAT);
+```
+
+## 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/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/docs/usage/configuration.md b/docs/usage/configuration.md
new file mode 100644
index 0000000..34ed5f1
--- /dev/null
+++ b/docs/usage/configuration.md
@@ -0,0 +1,137 @@
+# Configuration
+
+phpGPX is configured through two mechanisms:
+
+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.
+
+## 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
+use phpGPX\phpGPX;
+use phpGPX\Config;
+
+$gpx = new phpGPX(config: new Config(
+ // Pretty print XML and JSON output (default: true)
+ prettyPrint: true,
+));
+```
+
+## Default configuration
+
+All options have sensible defaults. Creating a `phpGPX` instance without arguments uses them:
+
+```php
+$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(engine: Engine::default(
+ applyElevationSmoothing: true,
+ elevationSmoothingThreshold: 5,
+));
+
+$raw = new phpGPX(engine: Engine::default());
+
+$smoothFile = $smooth->load('track.gpx');
+$rawFile = $raw->load('track.gpx');
+```
+
+## Notes
+
+- 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](extensions.md) for custom extension setup.
\ No newline at end of file
diff --git a/docs/usage/creating-files.md b/docs/usage/creating-files.md
new file mode 100644
index 0000000..18ad0a6
--- /dev/null
+++ b/docs/usage/creating-files.md
@@ -0,0 +1,163 @@
+# 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\PointType;
+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(PointType::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;
+$gpxFile->tracks[] = $track;
+
+// 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\Models\GpxFile;
+use phpGPX\Models\Point;
+use phpGPX\Models\PointType;
+use phpGPX\Models\Route;
+use phpGPX\phpGPX;
+
+$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(PointType::Routepoint);
+ $point->latitude = $data['lat'];
+ $point->longitude = $data['lon'];
+ $point->elevation = $data['ele'];
+ $point->name = $data['name'];
+ $route->points[] = $point;
+}
+
+$gpxFile->routes[] = $route;
+$gpxFile->save('trail.gpx', phpGPX::XML_FORMAT);
+```
+
+## Adding waypoints
+
+```php
+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(PointType::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::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
+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/usage/extensions.md b/docs/usage/extensions.md
new file mode 100644
index 0000000..f9ea8a6
--- /dev/null
+++ b/docs/usage/extensions.md
@@ -0,0 +1,194 @@
+# Extensions
+
+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
+
+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
+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) {
+ $ext = $point->extensions?->get(TrackPointExtension::class);
+ if ($ext !== null) {
+ 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->set($ext);
+
+$point->extensions = $extensions;
+```
+
+The correct XML namespaces are handled automatically during serialization.
+
+## Unsupported extensions
+
+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']
+```
+
+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\ExtensionInterface;
+
+class MyExtension 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;
+ }
+}
+```
+
+### 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](statistics.md) for details.
+
+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/usage/loading-files.md b/docs/usage/loading-files.md
new file mode 100644
index 0000000..54db9bc
--- /dev/null
+++ b/docs/usage/loading-files.md
@@ -0,0 +1,80 @@
+# Loading Files
+
+## From file path
+
+The simplest way to load a GPX file:
+
+```php
+use phpGPX\phpGPX;
+
+$gpx = new phpGPX();
+$file = $gpx->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
+
+';
+
+$gpx = new phpGPX();
+$file = $gpx->parse($xml);
+```
+
+## With statistics
+
+Statistics are not calculated by default. Pass a `engine` to populate `$track->stats`, `$segment->stats`, and `$route->stats`:
+
+```php
+use phpGPX\phpGPX;
+use phpGPX\Analysis\Engine;
+
+$gpx = new phpGPX(engine: Engine::default());
+
+$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
+
+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** - 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
+
+After parsing, the `engine` (if provided) runs a single-pass analysis over all points:
+
+```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/usage/statistics.md b/docs/usage/statistics.md
new file mode 100644
index 0000000..18a055a
--- /dev/null
+++ b/docs/usage/statistics.md
@@ -0,0 +1,191 @@
+# Statistics
+
+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 | 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(engine: Engine::default());
+$file = $gpx->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";
+ }
+}
+```
+
+## The engine
+
+The engine walks the GPX structure **once** and dispatches each point to all registered analyzers in a single pass. No redundant iteration.
+
+### Quick start with defaults
+
+```php
+$gpx = new phpGPX(engine: Engine::default());
+```
+
+### Customizing via the factory
+
+```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);
+```
+
+## Built-in analyzers
+
+### DistanceAnalyzer
+
+Computes raw (2D) and real (3D) distance via the Haversine formula.
+
+```php
+new DistanceAnalyzer(
+ applySmoothing: true, // filter GPS jitter
+ smoothingThreshold: 2, // meters — ignore movements below 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
+)
+```
+
+### AltitudeAnalyzer
+
+Finds minimum and maximum altitude with coordinates.
+
+```php
+new AltitudeAnalyzer(ignoreZeroElevation: true)
+```
+
+### TimestampAnalyzer
+
+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
+```
+
+### TrackPointExtensionAnalyzer
+
+Aggregates Garmin TrackPointExtension sensor data (heart rate, cadence, temperature).
+
+## 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\phpGPX;
+use phpGPX\Analysis\Engine;
+
+$gpx = new phpGPX(engine: Engine::default(
+ sortByTimestamp: true,
+ applyElevationSmoothing: true,
+ elevationSmoothingThreshold: 2,
+));
+
+$file = $gpx->load('track.gpx');
+```
+
+```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/example/Example.php b/example/Example.php
deleted file mode 100644
index c08b5f0..0000000
--- a/example/Example.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- */
-
-use phpGPX\phpGPX;
-
-require_once '../vendor/autoload.php';
-
-$gpx = new phpGPX();
-$file = $gpx->load('endomondo.gpx');
-
-phpGPX::$PRETTY_PRINT = true;
-//$file->save('output_Evening_Ride.gpx', phpGPX::XML_FORMAT);
-
-foreach ($file->tracks as $track) {
- var_dump($track->stats->toArray());
-}
diff --git a/example/CreateFileFromScratch.php b/examples/CreateFileFromScratch.php
similarity index 78%
rename from example/CreateFileFromScratch.php
rename to examples/CreateFileFromScratch.php
index b60f0cd..30a31e3 100644
--- a/example/CreateFileFromScratch.php
+++ b/examples/CreateFileFromScratch.php
@@ -1,16 +1,18 @@
*/
+use phpGPX\Models\Extensions;
+use phpGPX\Models\Extensions\TrackPointExtension;
use phpGPX\Models\GpxFile;
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;
-use phpGPX\Models\Extensions\TrackPointExtension;
require_once '../vendor/autoload.php';
@@ -20,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
@@ -60,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
@@ -70,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();
@@ -84,7 +86,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'];
@@ -94,7 +96,7 @@
$point->extensions = new Extensions();
$trackPointExtension = new TrackPointExtension();
$trackPointExtension->aTemp = $sample_point['aTemp'];
- $point->extensions->trackPointExtension = $trackPointExtension;
+ $point->extensions->set($trackPointExtension);
$segment->points[] = $point;
}
@@ -106,7 +108,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'];
@@ -124,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
new file mode 100644
index 0000000..a3cdc20
--- /dev/null
+++ b/examples/Example.php
@@ -0,0 +1,30 @@
+
+ */
+
+use phpGPX\Analysis\Engine;
+use phpGPX\Config;
+use phpGPX\phpGPX;
+
+require_once '../vendor/autoload.php';
+
+$gpx = new phpGPX(
+ config: new Config(prettyPrint: true),
+ engine: Engine::default(),
+);
+
+$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 "\nFull stats:\n";
+ var_dump($track->stats->jsonSerialize());
+}
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 77%
rename from example/waypoints_create.php
rename to examples/waypoints_create.php
index bfd235e..2aba9a3 100644
--- a/example/waypoints_create.php
+++ b/examples/waypoints_create.php
@@ -1,4 +1,5 @@
*/
@@ -7,6 +8,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;
@@ -17,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
@@ -54,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
@@ -64,18 +66,18 @@
$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) {
// 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/example/waypoints_load.php b/examples/waypoints_load.php
similarity index 64%
rename from example/waypoints_load.php
rename to examples/waypoints_load.php
index b9574d9..89e76e1 100644
--- a/example/waypoints_load.php
+++ b/examples/waypoints_load.php
@@ -1,29 +1,28 @@
*/
+use phpGPX\Config;
use phpGPX\phpGPX;
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";
}
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..b2a3e00
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,89 @@
+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
+edit_uri: edit/develop/docs/
+
+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.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
+ - pymdownx.details
+ - 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
+ - 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/phpunit.xml b/phpunit.xml
index aa62e76..d49233d 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,25 +1,23 @@
+
+
+
+ tests/Unit
+
+
+ tests/Integration
+
+
-
-
-
-
-
-
-
-
-
- tests/
-
-
-
-
+
+
+ src
+
+
+
\ 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..d8a9c28
--- /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.
+ }
+}
diff --git a/src/phpGPX/Analysis/AltitudeAnalyzer.php b/src/phpGPX/Analysis/AltitudeAnalyzer.php
new file mode 100644
index 0000000..7e6b493
--- /dev/null
+++ b/src/phpGPX/Analysis/AltitudeAnalyzer.php
@@ -0,0 +1,103 @@
+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;
+ }
+ }
+ }
+}
diff --git a/src/phpGPX/Analysis/BoundsAnalyzer.php b/src/phpGPX/Analysis/BoundsAnalyzer.php
new file mode 100644
index 0000000..587999b
--- /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;
+ }
+ }
+}
diff --git a/src/phpGPX/Analysis/DistanceAnalyzer.php b/src/phpGPX/Analysis/DistanceAnalyzer.php
new file mode 100644
index 0000000..2d0cf59
--- /dev/null
+++ b/src/phpGPX/Analysis/DistanceAnalyzer.php
@@ -0,0 +1,107 @@
+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;
+ }
+}
diff --git a/src/phpGPX/Analysis/ElevationAnalyzer.php b/src/phpGPX/Analysis/ElevationAnalyzer.php
new file mode 100644
index 0000000..333e861
--- /dev/null
+++ b/src/phpGPX/Analysis/ElevationAnalyzer.php
@@ -0,0 +1,119 @@
+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;
+ }
+}
diff --git a/src/phpGPX/Analysis/Engine.php b/src/phpGPX/Analysis/Engine.php
new file mode 100644
index 0000000..225adc3
--- /dev/null
+++ b/src/phpGPX/Analysis/Engine.php
@@ -0,0 +1,308 @@
+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;
+ }
+ }
+}
diff --git a/src/phpGPX/Analysis/MovementAnalyzer.php b/src/phpGPX/Analysis/MovementAnalyzer.php
new file mode 100644
index 0000000..8b9acc6
--- /dev/null
+++ b/src/phpGPX/Analysis/MovementAnalyzer.php
@@ -0,0 +1,100 @@
+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
+ }
+}
diff --git a/src/phpGPX/Analysis/PointAnalyzerInterface.php b/src/phpGPX/Analysis/PointAnalyzerInterface.php
new file mode 100644
index 0000000..9ea3e73
--- /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;
+}
diff --git a/src/phpGPX/Analysis/TimestampAnalyzer.php b/src/phpGPX/Analysis/TimestampAnalyzer.php
new file mode 100644
index 0000000..a8ea7bb
--- /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;
+ }
+ }
+ }
+}
diff --git a/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php
new file mode 100644
index 0000000..8e6661e
--- /dev/null
+++ b/src/phpGPX/Analysis/TrackPointExtensionAnalyzer.php
@@ -0,0 +1,154 @@
+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?->get(TrackPointExtension::class);
+
+ 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;
+ }
+}
diff --git a/src/phpGPX/Config.php b/src/phpGPX/Config.php
new file mode 100644
index 0000000..4e880da
--- /dev/null
+++ b/src/phpGPX/Config.php
@@ -0,0 +1,17 @@
+
@@ -6,41 +7,26 @@
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)
- {
- if ($point1->time == $point2->time) {
- return 0;
- }
- return $point1->time > $point2->time;
- }
-
/**
* @param $datetime
* @param string $format
- * @param string $timezone
+ * @param string|null $timezone
* @return null|string
+ * @throws \Exception
*/
- public static function formatDateTime($datetime, $format = 'c', $timezone = 'UTC')
+ public static function formatDateTime($datetime, string $format = 'c', ?string $timezone = 'UTC'): ?string
{
- $formatted = null;
+ $formatted = null;
if ($datetime instanceof \DateTime) {
- $datetime->setTimezone(new \DateTimeZone($timezone));
- $formatted = $datetime->format($format);
+ $datetime->setTimezone(new \DateTimeZone($timezone ?? 'UTC'));
+ $formatted = $datetime->format($format);
}
return $formatted;
@@ -50,8 +36,9 @@ public static function formatDateTime($datetime, $format = 'c', $timezone = 'UTC
* @param $value
* @param string $timezone
* @return \DateTime
+ * @throws \Exception
*/
- public static function parseDateTime($value, $timezone = 'Europe/London')
+ public static function parseDateTime($value, string $timezone = 'Europe/London'): \DateTime
{
$timezone = new \DateTimeZone($timezone);
$datetime = new \DateTime($value, $timezone);
diff --git a/src/phpGPX/Helpers/DistanceCalculator.php b/src/phpGPX/Helpers/DistanceCalculator.php
index 5a2a51b..c392acd 100644
--- a/src/phpGPX/Helpers/DistanceCalculator.php
+++ b/src/phpGPX/Helpers/DistanceCalculator.php
@@ -1,4 +1,5 @@
points = $points;
+ public function __construct(
+ private array $points,
+ private bool $applySmoothing = false,
+ private int $smoothingThreshold = 2,
+ ) {
}
- 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)
+ private function calculate(array $strategy): float
{
$distance = 0;
@@ -54,27 +46,21 @@ private function calculate($strategy)
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->applySmoothing) {
$differenceFromLastConsideredPoint = call_user_func($strategy, $curPoint, $lastConsideredPoint);
- if ($differenceFromLastConsideredPoint > phpGPX::$DISTANCE_SMOOTHING_THRESHOLD) {
+ if ($differenceFromLastConsideredPoint > $this->smoothingThreshold) {
$distance += $differenceFromLastConsideredPoint;
$lastConsideredPoint = $curPoint;
}
- }
-
- // if smoothing is not applied we consider every point
- else {
+ } else {
$distance += $curPoint->difference;
$lastConsideredPoint = $curPoint;
}
diff --git a/src/phpGPX/Helpers/ElevationGainLossCalculator.php b/src/phpGPX/Helpers/ElevationGainLossCalculator.php
index 7d80e11..822048e 100644
--- a/src/phpGPX/Helpers/ElevationGainLossCalculator.php
+++ b/src/phpGPX/Helpers/ElevationGainLossCalculator.php
@@ -1,4 +1,5 @@
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 ($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 ($applySmoothing &&
+ abs($elevationDelta) > $smoothingThreshold &&
+ ($spikesThreshold === null || abs($elevationDelta) < $spikesThreshold)) {
$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 (!$applySmoothing) {
$cumulativeElevationGain += ($elevationDelta > 0) ? $elevationDelta : 0;
$cumulativeElevationLoss += ($elevationDelta < 0) ? abs($elevationDelta) : 0;
diff --git a/src/phpGPX/Helpers/GeoHelper.php b/src/phpGPX/Helpers/GeoHelper.php
index adaabe1..ae7f6b5 100644
--- a/src/phpGPX/Helpers/GeoHelper.php
+++ b/src/phpGPX/Helpers/GeoHelper.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.
@@ -24,7 +25,7 @@ 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);
@@ -45,7 +46,7 @@ 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);
diff --git a/src/phpGPX/Helpers/SerializationHelper.php b/src/phpGPX/Helpers/SerializationHelper.php
index 433e950..239efd5 100644
--- a/src/phpGPX/Helpers/SerializationHelper.php
+++ b/src/phpGPX/Helpers/SerializationHelper.php
@@ -1,4 +1,5 @@
@@ -6,80 +7,26 @@
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
{
-
- /**
- * Returns integer or null.
- * @param $value
- * @return int|null
- */
- public static function integerOrNull($value)
- {
- return is_numeric($value) ? (integer) $value : null;
- }
-
- /**
- * Returns float or null.
- * @param $value
- * @return float|null
- */
- public static function floatOrNull($value)
- {
- return is_numeric($value) ? (float) $value : null;
- }
-
- /**
- * Returns string or null
- * @param $value
- * @return null|string
- */
- public static function stringOrNull($value)
- {
- return is_string($value) ? $value : null;
- }
-
/**
- * Recursively traverse Summarizable objects and returns their array representation according summary() method.
- * @param Summarizable|Summarizable[] $object
- * @return array|null
+ * 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 serialize($object)
+ public static function position(?float $longitude, ?float $latitude, ?float $elevation = null): array
{
- if (is_array($object)) {
- $result = [];
- foreach ($object as $record) {
- $result[] = $record->toArray();
- $record = null;
- }
- $object = null;
- return $result;
- } else {
- return $object != null ? $object->toArray() : null;
+ $pos = [(float) $longitude, (float) $latitude];
+ if ($elevation !== null) {
+ $pos[] = $elevation;
}
- }
-
- public static function filterNotNull(array $array)
- {
- foreach ($array as &$item) {
- if (!is_array($item)) {
- continue;
- }
-
- $item = self::filterNotNull($item);
- }
-
- $array = array_filter($array, function ($item) {
- return $item !== null && (!is_array($item) || count($item));
- });
-
- return $array;
+ return $pos;
}
}
diff --git a/src/phpGPX/Models/Bounds.php b/src/phpGPX/Models/Bounds.php
index e2d41c3..88ffe4d 100644
--- a/src/phpGPX/Models/Bounds.php
+++ b/src/phpGPX/Models/Bounds.php
@@ -1,61 +1,41 @@
- */
namespace phpGPX\Models;
-class Bounds implements Summarizable
+/**
+ * Two lat/lon pairs defining the extent of an element.
+ */
+class Bounds implements \JsonSerializable
{
+ public const TAG_NAME = 'bounds';
+
+ public function __construct(
+ public ?float $minLatitude = null,
+ public ?float $minLongitude = null,
+ public ?float $maxLatitude = null,
+ public ?float $maxLongitude = null,
+ ) {
+ }
/**
- * Minimal latitude in file.
- * @var float
- */
- public $minLatitude;
-
- /**
- * Minimal longitude in file.
- * @var float
- */
- public $minLongitude;
-
- /**
- * Maximal latitude in file.
- * @var float
- */
- public $maxLatitude;
-
- /**
- * Maximal longitude in file.
- * @var float
- */
- public $maxLongitude;
-
- /**
- * Bounds constructor.
+ * GeoJSON bbox: [minLon, minLat, maxLon, maxLat]
*/
- public function __construct()
+ public function jsonSerialize(): array
{
- $this->minLatitude = null;
- $this->minLongitude = null;
- $this->maxLongitude = null;
- $this->maxLatitude = null;
+ return [$this->minLongitude, $this->minLatitude, $this->maxLongitude, $this->maxLatitude];
}
-
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public static function parse(\SimpleXMLElement $node): ?Bounds
{
- return [
- 'minlat' => $this->minLatitude,
- 'minlon' => $this->minLongitude,
- 'maxlat' => $this->maxLatitude,
- 'maxlon' => $this->maxLongitude
- ];
+ 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/Collection.php b/src/phpGPX/Models/Collection.php
index 8fabf7a..6875962 100644
--- a/src/phpGPX/Models/Collection.php
+++ b/src/phpGPX/Models/Collection.php
@@ -1,99 +1,39 @@
- */
namespace phpGPX\Models;
/**
- * Class Collection
- * @package phpGPX\Models
+ * Abstract base class for Track and Route.
*/
-abstract class Collection implements Summarizable, StatsCalculator
+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 $name;
-
- /**
- * GPS comment for route.
- * An original GPX 1.1 attribute.
- * @var string|null
- */
- public $comment;
-
- /**
- * Text description of route/track for user. Not sent to GPS.
- * An original GPX 1.1 attribute.
- * @var string|null
- */
- public $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;
+ /** 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 $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 $number;
+ /** Source of data. */
+ public ?string $source = null;
- /**
- * Type (classification) of route/track.
- * An original GPX 1.1 attribute.
- * @var string|null
- */
- public $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;
+ /** GPS route/track number. */
+ public ?int $number = null;
- /**
- * Objects contains calculated statistics for collection.
- * @var Stats|null
- */
- public $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;
- }
+ /** GPX extensions. */
+ public ?Extensions $extensions = null;
+ /** Calculated statistics. */
+ public ?Stats $stats = null;
- /**
- * Return all points in collection.
- * @return Point[]
- */
- abstract public function getPoints();
+ /** @return Point[] */
+ abstract public function getPoints(): array;
}
diff --git a/src/phpGPX/Models/Copyright.php b/src/phpGPX/Models/Copyright.php
index 78d1836..c344527 100644
--- a/src/phpGPX/Models/Copyright.php
+++ b/src/phpGPX/Models/Copyright.php
@@ -1,61 +1,25 @@
- */
namespace phpGPX\Models;
-use phpGPX\Helpers\SerializationHelper;
-
/**
- * 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 Summarizable
+class Copyright implements \JsonSerializable
{
-
- /**
- * Copyright holder (TopoSoft, Inc.)
- * @var string
- */
- public $author;
-
- /**
- * Year of copyright.
- * @var string
- */
- public $year;
-
- /**
- * Link to external file containing license text.
- * @var string
- */
- public $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,
+ ) {
}
-
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): array
{
- return [
+ return array_filter([
'author' => $this->author,
- 'year' => SerializationHelper::stringOrNull($this->year),
- 'license' => SerializationHelper::stringOrNull($this->license)
- ];
+ '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 fbb0155..5488e6b 100644
--- a/src/phpGPX/Models/Email.php
+++ b/src/phpGPX/Models/Email.php
@@ -1,49 +1,23 @@
- */
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 Summarizable
+class Email implements \JsonSerializable
{
-
- /**
- * Id half of email address (jakub.dubec)
- * @var string
- */
- public $id;
-
- /** Domain half of email address (gmail.com)
- * @var string
- */
- public $domain;
-
- /**
- * Email constructor.
- */
- public function __construct()
- {
- $this->id = null;
- $this->domain = null;
+ public function __construct(
+ public ?string $id = null,
+ public ?string $domain = null,
+ ) {
}
-
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): array
{
- return [
- 'id' => (string) $this->id,
- 'domain' => (string) $this->domain
- ];
+ 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 5eb37fa..c4db456 100644
--- a/src/phpGPX/Models/Extensions.php
+++ b/src/phpGPX/Models/Extensions.php
@@ -1,42 +1,100 @@
- */
namespace phpGPX\Models;
-use phpGPX\Helpers\SerializationHelper;
-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 Summarizable
+class Extensions implements \JsonSerializable
{
+ /** @var array, ExtensionInterface> */
+ private array $items = [];
+
+ /**
+ * Unsupported extensions preserved as key-value pairs.
+ * Keys are prefixed element names (e.g., "ns:ElementName"), values are string content.
+ * @var array
+ */
+ public array $unsupported = [];
+
+ /**
+ * Store a typed extension.
+ */
+ public function set(ExtensionInterface $extension): void
+ {
+ $this->items[get_class($extension)] = $extension;
+ }
+
+ /**
+ * Retrieve a typed extension by class name.
+ *
+ * @template T of ExtensionInterface
+ * @param class-string $class
+ * @return T|null
+ */
+ public function get(string $class): ?ExtensionInterface
+ {
+ return $this->items[$class] ?? null;
+ }
+
/**
- * GPX Garmin TrackPointExtension v1
- * @see 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1'
- * @var TrackPointExtension
+ * Check if a typed extension is present.
+ *
+ * @param class-string $class
*/
- public $trackPointExtension;
+ public function has(string $class): bool
+ {
+ return isset($this->items[$class]);
+ }
/**
- * @var []
+ * Get all typed extensions.
+ *
+ * @return array, ExtensionInterface>
*/
- public $unsupported = [];
+ public function all(): array
+ {
+ return $this->items;
+ }
/**
- * Serialize object to array
- * @return array
+ * Check if this container has any data (typed or unsupported).
*/
- public function toArray()
+ public function isEmpty(): bool
{
- return [
- 'trackpoint' => SerializationHelper::serialize($this->trackPointExtension),
- 'unsupported' => $this->unsupported,
- ];
+ 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();
}
}
diff --git a/src/phpGPX/Models/Extensions/AbstractExtension.php b/src/phpGPX/Models/Extensions/AbstractExtension.php
deleted file mode 100644
index fe5ae8b..0000000
--- a/src/phpGPX/Models/Extensions/AbstractExtension.php
+++ /dev/null
@@ -1,36 +0,0 @@
-
- */
-
-namespace phpGPX\Models\Extensions;
-
-use phpGPX\Models\Summarizable;
-
-abstract class AbstractExtension implements Summarizable
-{
-
- /**
- * XML namespace of extension
- * @var string
- */
- public $namespace;
-
- /**
- * Node name extension.
- * @var string
- */
- public $extensionName;
-
- /**
- * AbstractExtension constructor.
- * @param string $namespace
- * @param string $extensionName
- */
- public function __construct($namespace, $extensionName)
- {
- $this->namespace = $namespace;
- $this->extensionName = $extensionName;
- }
-}
diff --git a/src/phpGPX/Models/Extensions/ExtensionInterface.php b/src/phpGPX/Models/Extensions/ExtensionInterface.php
new file mode 100644
index 0000000..792eac1
--- /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;
+}
diff --git a/src/phpGPX/Models/Extensions/TrackPointExtension.php b/src/phpGPX/Models/Extensions/TrackPointExtension.php
index 53b8dd6..749988e 100644
--- a/src/phpGPX/Models/Extensions/TrackPointExtension.php
+++ b/src/phpGPX/Models/Extensions/TrackPointExtension.php
@@ -1,4 +1,5 @@
@@ -6,120 +7,67 @@
namespace phpGPX\Models\Extensions;
-use phpGPX\Helpers\SerializationHelper;
-
/**
- * 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 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 EXTENSION_NAMESPACE = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2';
- const EXTENSION_NAMESPACE_XSD = 'http://www.garmin.com/xmlschemas/TrackPointExtensionv2.xsd';
-
- const EXTENSION_NAME = 'TrackPointExtension';
- const EXTENSION_NAMESPACE_PREFIX = 'gpxtpx';
+ 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';
- /**
- * 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
- */
- public $wTemp;
-
- /**
- * Depth in meters.
- * @var float
- */
- public $depth;
+ 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;
+ }
- /**
- * 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;
+ /** Air temperature in degrees Celsius. */
+ public ?float $aTemp = null;
- /**
- * Heart rate in beats per minute.
- * @since v1.0RC3
- * @var float
- */
- public $hr;
+ /** Water temperature in degrees Celsius. */
+ public ?float $wTemp = null;
- /**
- * 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;
+ /** Depth in meters. */
+ public ?float $depth = null;
- /**
- * Cadence in revolutions per minute.
- * @var float
- */
- public $cad;
+ /** Heart rate in beats per minute. */
+ public ?float $hr = null;
- /**
- * Speed in meters per second.
- * @var float
- */
- public $speed;
+ /** Cadence in revolutions per minute. */
+ public ?float $cad = null;
- /**
- * Course. This type contains an angle measured in degrees in a clockwise direction from the true north line.
- * @var int
- */
- public $course;
+ /** Speed in meters per second. */
+ public ?float $speed = null;
- /**
- * Bearing. This type contains an angle measured in degrees in a clockwise direction from the true north line.
- * @var int
- */
- public $bearing;
+ /** Course in degrees from true north. */
+ public ?int $course = null;
- /**
- * TrackPointExtension constructor.
- */
- public function __construct()
- {
- parent::__construct(self::EXTENSION_NAMESPACE, self::EXTENSION_NAME);
- }
+ /** Bearing in degrees from true north. */
+ public ?int $bearing = null;
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): 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)
- ];
+ return array_filter([
+ '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);
}
}
diff --git a/src/phpGPX/Models/GpxFile.php b/src/phpGPX/Models/GpxFile.php
index abc3a6e..61393db 100644
--- a/src/phpGPX/Models/GpxFile.php
+++ b/src/phpGPX/Models/GpxFile.php
@@ -1,4 +1,5 @@
@@ -6,8 +7,9 @@
namespace phpGPX\Models;
-use phpGPX\Helpers\SerializationHelper;
+use phpGPX\Config;
use phpGPX\Parsers\ExtensionParser;
+use phpGPX\Parsers\ExtensionRegistry;
use phpGPX\Parsers\MetadataParser;
use phpGPX\Parsers\PointParser;
use phpGPX\Parsers\RouteParser;
@@ -19,96 +21,83 @@
* Representation of GPX file.
* @package phpGPX\Models
*/
-class GpxFile implements Summarizable
+class GpxFile implements \JsonSerializable
{
- /**
- * A list of waypoints.
- * @var Point[]
- */
- public $waypoints;
+ /** @var Point[] */
+ public array $waypoints = [];
- /**
- * A list of routes.
- * @var Route[]
- */
- public $routes;
+ /** @var Route[] */
+ public array $routes = [];
- /**
- * A list of tracks.
- * @var Track[]
- */
- public $tracks;
+ /** @var Track[] */
+ public array $tracks = [];
- /**
- * Metadata about the file.
- * The original GPX 1.1 attribute.
- * @var Metadata|null
- */
- public $metadata;
+ public ?Metadata $metadata = null;
- /**
- * @var Extensions|null
- */
- public $extensions;
+ public ?Extensions $extensions = null;
- /**
- * Creator of GPX file.
- * @var string|null
- */
- public $creator;
+ public ?string $creator = null;
- /**
- * GpxFile constructor.
- */
- public function __construct()
- {
- $this->waypoints = [];
- $this->routes = [];
- $this->tracks = [];
- $this->metadata = null;
- $this->extensions = null;
- $this->creator = null;
- }
+ public ?string $version = null;
+ public function __construct(
+ public readonly Config $config = new Config(),
+ ) {
+ }
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): 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)
- ]);
+ $features = [];
+
+ foreach ($this->waypoints as $waypoint) {
+ $features[] = $waypoint;
+ }
+
+ foreach ($this->routes as $route) {
+ $features[] = $route;
+ }
+
+ foreach ($this->tracks as $track) {
+ $features[] = $track;
+ }
+
+ $result = [
+ 'type' => 'FeatureCollection',
+ 'features' => $features,
+ ];
+
+ if ($this->metadata !== null) {
+ $result['properties'] = array_filter([
+ 'metadata' => $this->metadata,
+ 'creator' => $this->creator,
+ 'extensions' => $this->extensions,
+ ], fn ($v) => $v !== null);
+ }
+
+ return $result;
}
/**
- * Return JSON representation of GPX file with statistics.
- * @return string
+ * Return GeoJSON representation of GPX file.
*/
- public function toJSON()
+ public function toJSON(): string
{
- return json_encode($this->toArray(), phpGPX::$PRETTY_PRINT ? JSON_PRETTY_PRINT : null);
+ return json_encode($this, $this->config->prettyPrint ? JSON_PRETTY_PRINT : 0);
}
/**
* Create XML representation of GPX file.
- * @return \DOMDocument
*/
- public function toXML()
+ 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", "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();
if (!empty($this->metadata)) {
$gpx->appendChild(MetadataParser::toXML($this->metadata, $document));
@@ -126,21 +115,21 @@ public function toXML()
$gpx->appendChild(TrackParser::toXML($track, $document));
}
- if (!empty($this->extensions)) {
+ if ($this->extensions !== null && !$this->extensions->isEmpty()) {
$gpx->appendChild(ExtensionParser::toXML($this->extensions, $document));
}
// 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'];
@@ -150,12 +139,12 @@ public function toXML()
$gpx->setAttributeNS(
'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation',
- implode(" ", $schemaLocationArray)
+ implode(' ', $schemaLocationArray),
);
$document->appendChild($gpx);
- if (phpGPX::$PRETTY_PRINT) {
+ if ($this->config->prettyPrint) {
$document->formatOutput = true;
$document->preserveWhiteSpace = true;
}
@@ -164,10 +153,8 @@ public function toXML()
/**
* Save data to file according to selected format.
- * @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 +162,11 @@ public function save($path, $format)
$document->save($path);
break;
case phpGPX::JSON_FORMAT:
+ case phpGPX::GEOJSON_FORMAT:
file_put_contents($path, $this->toJSON());
break;
default:
- throw new \RuntimeException("Unsupported file format!");
- };
+ throw new \RuntimeException('Unsupported file format!');
+ }
}
}
diff --git a/src/phpGPX/Models/Link.php b/src/phpGPX/Models/Link.php
index bccdd3a..360568c 100644
--- a/src/phpGPX/Models/Link.php
+++ b/src/phpGPX/Models/Link.php
@@ -1,59 +1,26 @@
- */
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 Summarizable
+class Link implements \JsonSerializable
{
-
- /**
- * URL of hyperlink.
- * @var string
- */
- public $href;
-
- /**
- * Text of hyperlink.
- * @var string|null
- */
- public $text;
-
- /**
- * Mime type of content (image/jpeg)
- * @var string|null
- */
- public $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,
+ ) {
}
-
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): array
{
- return [
- 'href' => (string) $this->href,
+ return array_filter([
+ 'href' => $this->href,
'text' => $this->text,
- 'type' => $this->type
- ];
+ 'type' => $this->type,
+ ], fn ($v) => $v !== null);
}
}
diff --git a/src/phpGPX/Models/Metadata.php b/src/phpGPX/Models/Metadata.php
index 582b62a..b776ea1 100644
--- a/src/phpGPX/Models/Metadata.php
+++ b/src/phpGPX/Models/Metadata.php
@@ -1,115 +1,45 @@
- */
namespace phpGPX\Models;
use phpGPX\Helpers\DateTimeHelper;
-use phpGPX\Helpers\SerializationHelper;
/**
- * 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 Summarizable
+class Metadata implements \JsonSerializable
{
+ public ?string $name = null;
- /**
- * The name of the GPX file.
- * Original GPX 1.1 attribute.
- * @var string|null
- */
- public $name;
-
- /**
- * A description of the contents of the GPX file.
- * Original GPX 1.1 attribute.
- * @var string|null
- */
- public $description;
-
- /**
- * The person or organization who created the GPX file.
- * An original GPX 1.1 attribute.
- * @var Person|null
- */
- public $author;
+ public ?string $description = null;
- /**
- * Copyright and license information governing use of the file.
- * Original GPX 1.1 attribute.
- * @var Copyright|null
- */
- public $copyright;
+ public ?Person $author = null;
- /**
- * Original GPX 1.1 attribute.
- * @var Link[]|null
- */
- public $links;
+ public ?Copyright $copyright = null;
- /**
- * Date of GPX creation
- * @var \DateTime
- */
- public $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 $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;
+ public ?string $keywords = null;
- /**
- * Extensions.
- * @var Extensions|null
- */
- public $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;
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): 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),
+ 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' => SerializationHelper::stringOrNull($this->keywords),
- 'bounds' => SerializationHelper::serialize($this->bounds),
- 'extensions' => SerializationHelper::serialize($this->extensions)
- ];
+ '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 29e6675..04d99bf 100644
--- a/src/phpGPX/Models/Person.php
+++ b/src/phpGPX/Models/Person.php
@@ -1,63 +1,26 @@
- */
namespace phpGPX\Models;
-use phpGPX\Helpers\SerializationHelper;
-
/**
- * Class Person
- * A person or organisation
- * @package phpGPX\Models
+ * A person or organisation.
*/
-class Person implements Summarizable
+class Person implements \JsonSerializable
{
-
- /**
- * Name of person or organization.
- * An original GPX 1.1 attribute.
- * @var string
- */
- public $name;
-
- /**
- * E-mail address.
- * An original GPX 1.1 attribute.
- * @var Email|null
- */
- public $email;
-
- /**
- * Link to Web site or other external information about person.
- * An original GPX 1.1 attribute.
- * @var Link[]
- */
- public $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,
+ ) {
}
-
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): array
{
- return [
- 'name' => (string) $this->name,
- 'email' => SerializationHelper::serialize($this->email),
- 'links' => SerializationHelper::serialize($this->links)
- ];
+ 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 a8409e2..8654716 100644
--- a/src/phpGPX/Models/Point.php
+++ b/src/phpGPX/Models/Point.php
@@ -1,270 +1,126 @@
- */
namespace phpGPX\Models;
-use phpGPX\Helpers\SerializationHelper;
use phpGPX\Helpers\DateTimeHelper;
-use phpGPX\phpGPX;
+use phpGPX\Helpers\SerializationHelper;
/**
- * 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 Summarizable
+class Point implements \JsonSerializable
{
- const WAYPOINT = 'waypoint';
- const TRACKPOINT = 'track';
- const ROUTEPOINT = 'route';
+ /** The latitude of the point. Decimal degrees, WGS84 datum. */
+ public ?float $latitude = null;
- /**
- * The latitude of the point. Decimal degrees, WGS84 datum.
- * Original GPX 1.1 attribute.
- * @var float
- */
- public $latitude;
+ /** The longitude of the point. Decimal degrees, WGS84 datum. */
+ public ?float $longitude = null;
- /**
- * The longitude of the point. Decimal degrees, WGS84 datum.
- * Original GPX 1.1 attribute.
- * @var float
- */
- public $longitude;
+ /** Elevation (in meters) of the point. */
+ public ?float $elevation = null;
- /**
- * Elevation (in meters) of the point.
- * Original GPX 1.1 attribute.
- * @var float|null
- */
- public $elevation;
+ /** Creation/modification timestamp (UTC). */
+ public ?\DateTime $time = 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 $time;
+ /** Magnetic variation (in degrees) at the point. */
+ public ?float $magVar = null;
- /**
- * Magnetic variation (in degrees) at the point
- * Original GPX 1.1 attribute.
- * @var float|null
- */
- public $magVar;
+ /** Height (in meters) of geoid above WGS84 earth ellipsoid. */
+ public ?float $geoidHeight = 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 $geoidHeight;
+ /** The GPS name of the waypoint. */
+ public ?string $name = 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 $name;
+ /** GPS waypoint comment. */
+ public ?string $comment = null;
- /**
- * GPS waypoint comment. Sent to GPS as comment.
- * Original GPX 1.1 attribute.
- * @var string|null
- */
- public $comment;
+ /** Text description of the element. */
+ public ?string $description = 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 $description;
+ /** Source of data. */
+ public ?string $source = 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 $source;
+ /** @var Link[] Links to additional information about the waypoint. */
+ public array $links = [];
- /**
- * Link to additional information about the waypoint.
- * Original GPX 1.1 attribute.
- * @var Link[]
- */
- public $links;
+ /** Text of GPS symbol name. */
+ public ?string $symbol = 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 $symbol;
+ /** Type (classification) of the waypoint. */
+ public ?string $type = null;
- /**
- * Type (classification) of the waypoint.
- * Original GPX 1.1 attribute.
- * @var string|null
- */
- public $type;
+ /** Type of GPS fix. Possible values: none, 2d, 3d, dgps, pps. */
+ public ?string $fix = 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
- */
- public $fix;
+ /** Number of satellites used to calculate the GPX fix. */
+ public ?int $satellitesNumber = null;
- /**
- * Number of satellites used to calculate the GPX fix. Always positive value.
- * Original GPX 1.1 attribute.
- * @var integer
- */
- public $satellitesNumber;
+ /** Horizontal dilution of precision. */
+ public ?float $hdop = null;
- /**
- * Horizontal dilution of precision.
- * Original GPX 1.1 attribute.
- * @var float
- */
- public $hdop;
+ /** Vertical dilution of precision. */
+ public ?float $vdop = null;
- /**
- * Vertical dilution of precision.
- * Original GPX 1.1 attribute.
- * @var float
- */
- public $vdop;
+ /** Position dilution of precision. */
+ public ?float $pdop = null;
- /**
- * Position dilution of precision.
- * Original GPX 1.1 attribute
- * @var float
- */
- public $pdop;
+ /** Number of seconds since last DGPS update. */
+ public ?int $ageOfGpsData = null;
- /**
- * Number of seconds since last DGPS update.
- * Original GPX 1.1 attribute.
- * @var integer
- */
- public $ageOfGpsData;
+ /** ID of DGPS station used in differential correction. */
+ public ?int $dgpsid = 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
- */
- public $dgpsid;
+ /** Difference in distance (in meters) from previous point. Computed by phpGPX. */
+ public ?float $difference = null;
- /**
- * Difference in in distance (in meters) between last point.
- * Value is created by phpGPX library.
- * @var float
- */
- public $difference;
+ /** Distance from collection start in meters. Computed by phpGPX. */
+ public ?float $distance = null;
- /**
- * Distance from collection start in meters.
- * Value is created by phpGPX library.
- * @var float
- */
- public $distance;
+ /** GPX extensions. */
+ public ?Extensions $extensions = null;
- /**
- * Objects stores GPX extensions from another namespaces.
- * @var Extensions
- */
- public $extensions;
-
- /**
- * Type of the point (parent collation type (ROUTE|WAYPOINT|TRACK))
- * @var string
- */
- private $pointType;
-
- /**
- * Point constructor.
- * @param string $pointType
- */
- public function __construct($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()
+ public function getPointType(): PointType
{
return $this->pointType;
}
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): array
{
+ $properties = array_filter([
+ 'name' => $this->name,
+ 'ele' => $this->elevation,
+ 'time' => DateTimeHelper::formatDateTime($this->time),
+ '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 [
- '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)
+ 'type' => 'Feature',
+ 'geometry' => [
+ 'type' => 'Point',
+ 'coordinates' => SerializationHelper::position($this->longitude, $this->latitude, $this->elevation),
+ ],
+ 'properties' => $properties ?: new \stdClass(),
];
}
}
diff --git a/src/phpGPX/Models/PointType.php b/src/phpGPX/Models/PointType.php
new file mode 100644
index 0000000..882fafe
--- /dev/null
+++ b/src/phpGPX/Models/PointType.php
@@ -0,0 +1,10 @@
+
@@ -6,11 +7,7 @@
namespace phpGPX\Models;
-use phpGPX\Helpers\DistanceCalculator;
-use phpGPX\Helpers\ElevationGainLossCalculator;
-use phpGPX\Helpers\GeoHelper;
use phpGPX\Helpers\SerializationHelper;
-use phpGPX\phpGPX;
/**
* Class Route
@@ -18,124 +15,45 @@
*/
class Route extends Collection
{
-
- /**
- * A list of route points.
- * An original GPX 1.1 attribute.
- * @var Point[]
- */
- public $points;
-
- /**
- * Route constructor.
- */
- public function __construct()
- {
- parent::__construct();
- $this->points = [];
- }
+ /** @var Point[] */
+ public array $points = [];
/**
* Return all points in collection.
* @return Point[]
*/
- public function getPoints()
+ 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;
- }
-
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
- {
- 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)
- ];
+ return $this->points;
}
- /**
- * Recalculate stats objects.
- * @return void
- */
- public function recalculateStats()
+ public function jsonSerialize(): array
{
- if (empty($this->stats)) {
- $this->stats = new Stats();
- }
-
- $this->stats->reset();
-
- if (empty($this->points)) {
- return;
+ $coordinates = [];
+ foreach ($this->points as $point) {
+ $coordinates[] = SerializationHelper::position($point->longitude, $point->latitude, $point->elevation);
}
- $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());
-
- $calculator = new DistanceCalculator($this->getPoints());
- $this->stats->distance = $calculator->getRawDistance();
- $this->stats->realDistance = $calculator->getRealDistance();
+ $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);
- 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];
- }
-
- 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];
- }
-
- 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];
- }
- }
-
- if (($firstPoint->time instanceof \DateTime) && ($lastPoint->time instanceof \DateTime)) {
- $this->stats->duration = $lastPoint->time->getTimestamp() - $firstPoint->time->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);
- }
- }
+ return [
+ 'type' => 'Feature',
+ 'geometry' => [
+ 'type' => 'LineString',
+ 'coordinates' => $coordinates,
+ ],
+ 'properties' => $properties ?: new \stdClass(),
+ ];
}
}
diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php
index 0e34faf..f8680e3 100644
--- a/src/phpGPX/Models/Segment.php
+++ b/src/phpGPX/Models/Segment.php
@@ -1,131 +1,46 @@
- */
namespace phpGPX\Models;
-use phpGPX\Helpers\DistanceCalculator;
-use phpGPX\Helpers\ElevationGainLossCalculator;
-use phpGPX\Helpers\GeoHelper;
use phpGPX\Helpers\SerializationHelper;
-use phpGPX\phpGPX;
/**
- * 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 Summarizable, StatsCalculator
+class Segment implements \JsonSerializable
{
- /**
- * Array of segment points
- * @var Point[]
- */
- public $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;
+ public ?Extensions $extensions = null;
- /**
- * @var Stats|null
- */
- public $stats;
+ public ?Stats $stats = null;
- /**
- * Segment constructor.
- */
- public function __construct()
+ public function jsonSerialize(): array
{
- $this->points = [];
- $this->extensions = null;
- $this->stats = null;
- }
+ $coordinates = [];
+ foreach ($this->points as $point) {
+ $coordinates[] = SerializationHelper::position($point->longitude, $point->latitude, $point->elevation);
+ }
+ $properties = array_filter([
+ 'extensions' => $this->extensions,
+ 'stats' => $this->stats,
+ ], fn ($v) => $v !== null);
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
- {
return [
- 'points' => SerializationHelper::serialize($this->points),
- 'extensions' => SerializationHelper::serialize($this->extensions),
- 'stats' => SerializationHelper::serialize($this->stats)
+ 'type' => 'Feature',
+ 'geometry' => [
+ 'type' => 'LineString',
+ 'coordinates' => $coordinates,
+ ],
+ 'properties' => $properties ?: new \stdClass(),
];
}
- /**
- * @return array|Point[]
- */
- public function getPoints()
+ /** @return Point[] */
+ public function getPoints(): array
{
return $this->points;
}
-
- /**
- * Recalculate stats objects.
- * @return void
- */
- public function recalculateStats()
- {
- if (empty($this->stats)) {
- $this->stats = new Stats();
- }
-
- $count = count($this->points);
- $this->stats->reset();
-
- if (empty($this->points)) {
- 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());
-
- $calculator = new DistanceCalculator($this->getPoints());
- $this->stats->distance = $calculator->getRawDistance();
- $this->stats->realDistance = $calculator->getRealDistance();
-
- 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];
- }
-
- 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];
- }
- }
-
- 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->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);
- }
- }
- }
}
diff --git a/src/phpGPX/Models/Stats.php b/src/phpGPX/Models/Stats.php
index 89ae6ac..75f22c6 100644
--- a/src/phpGPX/Models/Stats.php
+++ b/src/phpGPX/Models/Stats.php
@@ -1,4 +1,5 @@
@@ -7,148 +8,170 @@
namespace phpGPX\Models;
use phpGPX\Helpers\DateTimeHelper;
-use phpGPX\phpGPX;
/**
* Class Stats
* @package phpGPX\Models
*/
-class Stats implements Summarizable
+class Stats implements \JsonSerializable
{
-
/**
* 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
+ * Coordinate bounds
+ * @var Bounds|null
*/
- public function reset()
- {
- $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;
- }
+ 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;
/**
- * Serialize object to array
- * @return array
+ * Average heart rate in beats per minute (bpm)
+ * @var float|null
*/
- public function toArray()
+ 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;
+
+ public function jsonSerialize(): array
{
- return [
- 'distance' => (float)$this->distance,
- 'realDistance' => (float)$this->realDistance,
- 'avgSpeed' => (float)$this->averageSpeed,
- 'avgPace' => (float)$this->averagePace,
- 'minAltitude' => (float)$this->minAltitude,
+ return array_filter([
+ 'distance' => $this->distance,
+ 'realDistance' => $this->realDistance,
+ 'avgSpeed' => $this->averageSpeed,
+ 'avgPace' => $this->averagePace,
+ 'minAltitude' => $this->minAltitude,
'minAltitudeCoords' => $this->minAltitudeCoords,
- 'maxAltitude' => (float)$this->maxAltitude,
+ 'maxAltitude' => $this->maxAltitude,
'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,
+ 'cumulativeElevationLoss' => $this->cumulativeElevationLoss,
+ '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' => (float)$this->duration
- ];
+ '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 f5cab09..0000000
--- a/src/phpGPX/Models/StatsCalculator.php
+++ /dev/null
@@ -1,23 +0,0 @@
-
- */
-
-namespace phpGPX\Models;
-
-interface StatsCalculator
-{
-
- /**
- * Recalculate stats objects.
- * @return void
- */
- public function recalculateStats();
-
- /**
- * Return all points in collection.
- * @return Point[]
- */
- public function getPoints();
-}
diff --git a/src/phpGPX/Models/Summarizable.php b/src/phpGPX/Models/Summarizable.php
deleted file mode 100644
index 1e4433b..0000000
--- a/src/phpGPX/Models/Summarizable.php
+++ /dev/null
@@ -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 271817a..fc0d5a1 100644
--- a/src/phpGPX/Models/Track.php
+++ b/src/phpGPX/Models/Track.php
@@ -1,4 +1,5 @@
@@ -6,9 +7,7 @@
namespace phpGPX\Models;
-use phpGPX\Helpers\GeoHelper;
use phpGPX\Helpers\SerializationHelper;
-use phpGPX\phpGPX;
/**
* Class Track
@@ -16,28 +15,15 @@
*/
class Track extends Collection
{
-
- /**
- * Array of Track segments
- * @var Segment[]
- */
- public $segments;
-
- /**
- * Track constructor.
- */
- public function __construct()
- {
- parent::__construct();
- $this->segments = [];
- }
+ /** @var Segment[] */
+ public array $segments = [];
/**
* Return all points in collection.
* @return Point[]
*/
- public function getPoints()
+ public function getPoints(): array
{
/** @var Point[] $points */
$points = [];
@@ -46,113 +32,39 @@ public function getPoints()
$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;
}
- /**
- * Serialize object to array
- * @return array
- */
- public function toArray()
+ public function jsonSerialize(): 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)
- ];
- }
-
- /**
- * Recalculate stats objects.
- * @return void
- */
- public function recalculateStats()
- {
- if (empty($this->stats)) {
- $this->stats = new Stats();
- }
-
- $this->stats->reset();
-
- if (empty($this->segments)) {
- return;
- }
-
- $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();
-
- $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;
-
- if ($this->stats->minAltitude === null) {
- $this->stats->minAltitude = $this->segments[$s]->stats->minAltitude;
- $this->stats->minAltitudeCoords = $this->segments[$s]->stats->minAltitudeCoords;
- }
- 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 ($this->stats->minAltitude > $this->segments[$s]->stats->minAltitude) {
- $this->stats->minAltitude = $this->segments[$s]->stats->minAltitude;
- $this->stats->minAltitudeCoords = $this->segments[$s]->stats->minAltitudeCoords;
+ $segmentCoordinates = [];
+ foreach ($this->segments as $segment) {
+ $coordinates = [];
+ foreach ($segment->points as $point) {
+ $coordinates[] = SerializationHelper::position($point->longitude, $point->latitude, $point->elevation);
}
+ $segmentCoordinates[] = $coordinates;
}
- if (($firstPoint->time instanceof \DateTime) && ($lastPoint->time instanceof \DateTime)) {
- $this->stats->duration = abs($lastPoint->time->getTimestamp() - $firstPoint->time->getTimestamp());
+ $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);
- 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);
- }
- }
+ return [
+ 'type' => 'Feature',
+ 'geometry' => [
+ 'type' => 'MultiLineString',
+ 'coordinates' => $segmentCoordinates,
+ ],
+ 'properties' => $properties ?: new \stdClass(),
+ ];
}
}
diff --git a/src/phpGPX/Parsers/AbstractParser.php b/src/phpGPX/Parsers/AbstractParser.php
new file mode 100644
index 0000000..c475fbb
--- /dev/null
+++ b/src/phpGPX/Parsers/AbstractParser.php
@@ -0,0 +1,156 @@
+ [
+ * '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;
+ }
+
+ // Skip empty Extensions containers — avoid emitting
+ if ($value instanceof \phpGPX\Models\Extensions && $value->isEmpty()) {
+ 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));
+ }
+ }
+}
diff --git a/src/phpGPX/Parsers/BoundsParser.php b/src/phpGPX/Parsers/BoundsParser.php
index ada5ebf..869fbf7 100644
--- a/src/phpGPX/Parsers/BoundsParser.php
+++ b/src/phpGPX/Parsers/BoundsParser.php
@@ -1,4 +1,5 @@
@@ -14,27 +15,25 @@
*/
abstract class BoundsParser
{
- private static $tagName = 'bounds';
+ private static string $tagName = 'bounds';
/**
* Parse data from XML.
* @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;
}
- $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'],
+ );
}
/**
@@ -43,23 +42,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/CopyrightParser.php b/src/phpGPX/Parsers/CopyrightParser.php
index d5d97d7..bac6856 100644
--- a/src/phpGPX/Parsers/CopyrightParser.php
+++ b/src/phpGPX/Parsers/CopyrightParser.php
@@ -1,4 +1,5 @@
@@ -14,13 +15,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 +41,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 371b86a..8e14aad 100644
--- a/src/phpGPX/Parsers/EmailParser.php
+++ b/src/phpGPX/Parsers/EmailParser.php
@@ -1,4 +1,5 @@
@@ -14,13 +15,13 @@
*/
abstract class EmailParser
{
- private static $tagName = 'email';
+ private static string $tagName = 'email';
/**
* @param \SimpleXMLElement $node
* @return Email
*/
- public static function parse(\SimpleXMLElement $node)
+ public static function parse(\SimpleXMLElement $node): Email
{
$email = new Email();
@@ -36,15 +37,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/ExtensionParser.php b/src/phpGPX/Parsers/ExtensionParser.php
index 4b6154b..8ffa2fd 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 $tagName = 'extensions';
+ public static string $tagName = 'extensions';
+
+ /** @var array */
+ public static array $usedNamespaces = [];
- public static $usedNamespaces = [];
+ /**
+ * The active extension registry. Set by phpGPX before parse/serialize operations.
+ */
+ public static ?ExtensionRegistry $registry = null;
/**
- * @param \SimpleXMLElement $nodes
- * @return Extensions
+ * Parse an `` XML element into an Extensions container.
*/
- public static function parse($nodes)
+ 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)
+ 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)) {
diff --git a/src/phpGPX/Parsers/ExtensionRegistry.php b/src/phpGPX/Parsers/ExtensionRegistry.php
new file mode 100644
index 0000000..b1cdcf9
--- /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');
+ }
+}
diff --git a/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php b/src/phpGPX/Parsers/Extensions/ExtensionParserInterface.php
new file mode 100644
index 0000000..c5155b5
--- /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;
+}
diff --git a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php
index bb5ec42..13e9e75 100644
--- a/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php
+++ b/src/phpGPX/Parsers/Extensions/TrackPointExtensionParser.php
@@ -1,4 +1,5 @@
@@ -6,100 +7,68 @@
namespace phpGPX\Parsers\Extensions;
+use phpGPX\Models\Extensions\ExtensionInterface;
use phpGPX\Models\Extensions\TrackPointExtension;
-use phpGPX\Parsers\ExtensionParser;
+use phpGPX\Parsers\AbstractParser;
-class TrackPointExtensionParser
+class TrackPointExtensionParser extends AbstractParser implements ExtensionParserInterface
{
- 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): ExtensionInterface
{
$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']);
- }
-
- // Remove in v1.0
- if ($key == 'hr') {
- $extension->heartRate = $extension->hr;
- }
-
- // Remove in v1.0
- if ($key == 'cad') {
- $extension->cadence = $extension->cad;
- }
-
- // Remove in v1.0
- if ($key == 'atemp') {
- $extension->avgTemperature = $extension->aTemp;
- }
- }
+ self::mapAttributesFromXML($node, $extension);
return $extension;
}
- /**
- * @param TrackPointExtension $extension
- * @param \DOMDocument $document
- * @return \DOMElement
- */
- public static function toXML(TrackPointExtension $extension, \DOMDocument &$document)
+ 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::$attributeMapper as $key => $attribute) {
- if (!is_null($extension->{$attribute['name']})) {
+ foreach (self::getAttributeMapper() as $key => $attribute) {
+ if (isset($extension->{$attribute['name']})) {
$child = $document->createElement(
- sprintf("%s:%s", TrackPointExtension::EXTENSION_NAMESPACE_PREFIX, $key),
- $extension->{$attribute['name']}
+ sprintf('%s:%s', $prefix, $key),
+ $extension->{$attribute['name']},
);
$node->appendChild($child);
}
diff --git a/src/phpGPX/Parsers/LinkParser.php b/src/phpGPX/Parsers/LinkParser.php
index 66906b5..8b63895 100644
--- a/src/phpGPX/Parsers/LinkParser.php
+++ b/src/phpGPX/Parsers/LinkParser.php
@@ -1,4 +1,5 @@
@@ -10,40 +11,22 @@
abstract class LinkParser
{
- private static $tagName = 'link';
-
- /**
- * @param \SimpleXMLElement[] $nodes
- * @return Link[]
- */
- public static function parse($nodes = [])
- {
- $links = [];
- 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;
- }
+ private static string $tagName = 'link';
/**
- * @param Link[] $links
- * @param \DOMDocument $document
- * @return \DOMElement[]
+ * Parse a single link node.
+ *
+ * @param \SimpleXMLElement $node
+ * @return Link
*/
- public static function toXMLArray(array $links, \DOMDocument &$document)
+ public static function parse(\SimpleXMLElement $node): Link
{
- $result = [];
+ $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;
- foreach ($links as $link) {
- $result[] = self::toXML($link, $document);
- }
-
- return $result;
+ return $link;
}
/**
@@ -51,18 +34,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 = $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..56b75a2 100644
--- a/src/phpGPX/Parsers/MetadataParser.php
+++ b/src/phpGPX/Parsers/MetadataParser.php
@@ -1,4 +1,5 @@
@@ -13,130 +14,101 @@
* 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'])) {
- $metadata->{$attribute['name']} = isset($node->$key) ? $node->$key : null;
- if (!is_null($metadata->{$attribute['name']})) {
- settype($metadata->{$attribute['name']}, $attribute['type']);
- }
- }
- 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);
}
}
diff --git a/src/phpGPX/Parsers/PersonParser.php b/src/phpGPX/Parsers/PersonParser.php
index b4b682f..75f4530 100644
--- a/src/phpGPX/Parsers/PersonParser.php
+++ b/src/phpGPX/Parsers/PersonParser.php
@@ -1,4 +1,5 @@
@@ -14,24 +15,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 +52,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 731a48d..de896ba 100644
--- a/src/phpGPX/Parsers/PointParser.php
+++ b/src/phpGPX/Parsers/PointParser.php
@@ -1,195 +1,142 @@
- */
namespace phpGPX\Parsers;
use phpGPX\Helpers\DateTimeHelper;
use phpGPX\Models\Point;
+use phpGPX\Models\PointType;
-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'
- ]
- ];
-
- private static $typeMapper = [
- 'trkpt' => Point::TRACKPOINT,
- 'wpt' => Point::WAYPOINT,
- 'rtept' => Point::ROUTEPOINT
- ];
-
- public static function parse(\SimpleXMLElement $node)
+ protected static function getAttributeMapper(): array
{
- if (!array_key_exists($node->getName(), self::$typeMapper)) {
+ 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,
+ ],
+ ];
+ }
+
+ public static function parse(\SimpleXMLElement $node): ?Point
+ {
+ $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;
- 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'])) {
- $point->{$attribute['name']} = isset($node->$key) ? $node->$key : null;
- if (!is_null($point->{$attribute['name']})) {
- settype($point->{$attribute['name']}, $attribute['type']);
- }
- }
- break;
- }
- }
+ self::mapAttributesFromXML($node, $point);
+
+ $point->time = isset($node->time) ? DateTimeHelper::parseDateTime($node->time) : null;
+
+ $mapper = self::getAttributeMapper();
+ $point->links = self::parseDelegated($node, 'link', $mapper['link']);
+ $point->extensions = self::parseDelegated($node, 'extensions', $mapper['extensions']);
return $point;
}
- /**
- * @param Point $point
- * @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);
-
- 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;
- break;
- }
-
- if (is_array($child)) {
- foreach ($child as $item) {
- $node->appendChild($item);
- }
- } else {
- $node->appendChild($child);
- }
- }
- }
+ $node = $document->createElement($point->getPointType()->value);
- return $node;
- }
+ if ($point->latitude !== null) {
+ $node->setAttribute('lat', $point->latitude);
+ }
+ if ($point->longitude !== null) {
+ $node->setAttribute('lon', $point->longitude);
+ }
- /**
- * @param array $points
- * @param \DOMDocument $document
- * @return \DOMElement[]
- */
- public static function toXMLArray(array $points, \DOMDocument &$document)
- {
- $result = [];
+ self::mapAttributesToXML($point, $document, $node);
- foreach ($points as $point) {
- $result[] = self::toXML($point, $document);
+ if ($point->time !== null) {
+ $child = $document->createElement('time', DateTimeHelper::formatDateTime($point->time));
+ $node->appendChild($child);
}
- return $result;
+ $mapper = self::getAttributeMapper();
+ self::serializeDelegated($point->links, $mapper['link'], $document, $node);
+ self::serializeDelegated($point->extensions, $mapper['extensions'], $document, $node);
+
+ return $node;
}
}
diff --git a/src/phpGPX/Parsers/RouteParser.php b/src/phpGPX/Parsers/RouteParser.php
index 5558cd1..fa02f20 100644
--- a/src/phpGPX/Parsers/RouteParser.php
+++ b/src/phpGPX/Parsers/RouteParser.php
@@ -1,4 +1,5 @@
@@ -7,96 +8,77 @@
namespace phpGPX\Parsers;
use phpGPX\Models\Route;
-use phpGPX\phpGPX;
/**
* 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'])) {
- $route->{$attribute['name']} = isset($node->$key) ? $node->$key : null;
- if (!is_null($route->{$attribute['name']})) {
- settype($route->{$attribute['name']}, $attribute['type']);
- }
- }
- break;
- }
- }
+ self::mapAttributesFromXML($node, $route);
- if (phpGPX::$CALCULATE_STATS) {
- $route->recalculateStats();
+ foreach (self::getAttributeMapper() as $key => $attribute) {
+ if (isset($attribute['parser'])) {
+ $route->{$attribute['name']} = self::parseDelegated($node, $key, $attribute);
+ }
}
$routes[] = $route;
@@ -110,55 +92,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;
- }
}
diff --git a/src/phpGPX/Parsers/SegmentParser.php b/src/phpGPX/Parsers/SegmentParser.php
index 33aa8e6..93786c9 100644
--- a/src/phpGPX/Parsers/SegmentParser.php
+++ b/src/phpGPX/Parsers/SegmentParser.php
@@ -1,4 +1,5 @@
@@ -7,7 +8,6 @@
namespace phpGPX\Parsers;
use phpGPX\Models\Segment;
-use phpGPX\phpGPX;
/**
* Class SegmentParser
@@ -15,40 +15,34 @@
*/
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();
- }
-
- $segments[] = $segment;
}
- return $segments;
+ $segment->extensions = isset($node->extensions) ? ExtensionParser::parse($node->extensions) : null;
+
+ return $segment;
}
/**
@@ -56,7 +50,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);
@@ -64,26 +58,10 @@ public static function toXML(Segment $segment, \DOMDocument &$document)
$node->appendChild(PointParser::toXML($point, $document));
}
- if (!empty($segment->extensions)) {
+ if ($segment->extensions !== null && !$segment->extensions->isEmpty()) {
$node->appendChild(ExtensionParser::toXML($segment->extensions, $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;
- }
}
diff --git a/src/phpGPX/Parsers/TrackParser.php b/src/phpGPX/Parsers/TrackParser.php
index 650c17e..2e30952 100644
--- a/src/phpGPX/Parsers/TrackParser.php
+++ b/src/phpGPX/Parsers/TrackParser.php
@@ -1,4 +1,5 @@
@@ -7,90 +8,77 @@
namespace phpGPX\Parsers;
use phpGPX\Models\Track;
-use phpGPX\phpGPX;
/**
* 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'])) {
- $track->{$attribute['name']} = isset($node->$key) ? $node->$key : null;
- if (!is_null($track->{$attribute['name']})) {
- settype($track->{$attribute['name']}, $attribute['type']);
- }
- }
- break;
- }
- }
+ self::mapAttributesFromXML($node, $track);
- if (phpGPX::$CALCULATE_STATS) {
- $track->recalculateStats();
+ foreach (self::getAttributeMapper() as $key => $attribute) {
+ if (isset($attribute['parser'])) {
+ $track->{$attribute['name']} = self::parseDelegated($node, $key, $attribute);
+ }
}
$tracks[] = $track;
@@ -104,55 +92,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;
- }
}
diff --git a/src/phpGPX/Parsers/WaypointParser.php b/src/phpGPX/Parsers/WaypointParser.php
index 659af6f..7f9364e 100644
--- a/src/phpGPX/Parsers/WaypointParser.php
+++ b/src/phpGPX/Parsers/WaypointParser.php
@@ -1,4 +1,5 @@
@@ -12,12 +13,11 @@
*/
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/src/phpGPX/phpGPX.php b/src/phpGPX/phpGPX.php
index 5d91fb9..d59f56f 100644
--- a/src/phpGPX/phpGPX.php
+++ b/src/phpGPX/phpGPX.php
@@ -1,4 +1,5 @@
@@ -6,7 +7,10 @@
namespace phpGPX;
+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;
@@ -18,134 +22,93 @@
*/
class phpGPX
{
- const JSON_FORMAT = 'json';
- const XML_FORMAT = 'xml';
+ public const JSON_FORMAT = 'json';
+ public const XML_FORMAT = 'xml';
+ public const GEOJSON_FORMAT = 'geojson';
- const PACKAGE_NAME = 'phpGPX';
- const VERSION = '1.3.0';
+ public const PACKAGE_NAME = 'phpGPX';
+ public const VERSION = '2.0.0-beta.1';
- /**
- * Create Stats object for each track, segment and route
- * @var bool
- */
- public static $CALCULATE_STATS = true;
+ public readonly Config $config;
- /**
- * 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;
+ private ?Engine $engine = null;
- /**
- * Default DateTime output format in JSON serialization.
- * @var string
- */
- public static $DATETIME_FORMAT = 'c';
+ private ExtensionRegistry $extensionRegistry;
- /**
- * 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;
+ 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();
+ }
/**
- * In stats elevation calculation: ignore points with an elevation of 0
- * This can happen with some GPS software adding a point with 0 elevation
+ * Set the stats engine for computing statistics after parsing.
*
- * @var bool
+ * @return $this Fluent interface
*/
- 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;
+ public function setEngine(Engine $engine): self
+ {
+ $this->engine = $engine;
+ return $this;
+ }
/**
- * if APPLY_DISTANCE_SMOOTHING is true
- * the minimum distance between considered points in meters
- * @var int
+ * 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 static $DISTANCE_SMOOTHING_THRESHOLD = 2;
+ public function registerExtension(string $namespace, string $parserClass, string $prefix = 'ext'): self
+ {
+ $this->extensionRegistry->register($namespace, $parserClass, $prefix);
+ return $this;
+ }
/**
- * Load GPX file.
- * @param $path
- * @return GpxFile
+ * Load GPX file from path.
*/
- public static function load($path)
+ 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 $xml
- * @return GpxFile
*/
- public static function parse($xml)
+ 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;
+ // Configure extension parser with our registry
+ ExtensionParser::$registry = $this->extensionRegistry;
- // Parse waypoints
- $gpx->waypoints = isset($xml->wpt) ? WaypointParser::parse($xml->wpt) : [];
+ $gpx = new GpxFile($this->config);
- // Parse tracks
- $gpx->tracks = isset($xml->trk) ? TrackParser::parse($xml->trk) : [];
+ $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) : [];
+ $gpx->routes = isset($xmlElement->rte) ? RouteParser::parse($xmlElement->rte) : [];
- // Parse routes
- $gpx->routes = isset($xml->rte) ? RouteParser::parse($xml->rte) : [];
+ if ($this->engine !== null) {
+ $gpx = $this->engine->process($gpx);
+ }
return $gpx;
}
/**
* 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);
+ return sprintf('%s/%s', self::PACKAGE_NAME, self::VERSION);
}
}
diff --git a/tests/CreateWaypointTest.php b/tests/CreateWaypointTest.php
deleted file mode 100644
index 6b2ab9e..0000000
--- a/tests/CreateWaypointTest.php
+++ /dev/null
@@ -1,124 +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}");
- }
- 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/tests/Fixtures/Parsers/Bounds/bounds.json b/tests/Fixtures/Parsers/Bounds/bounds.json
new file mode 100644
index 0000000..e91dc9a
--- /dev/null
+++ b/tests/Fixtures/Parsers/Bounds/bounds.json
@@ -0,0 +1 @@
+[ 18.814543, 49.072489, 18.886939, 49.090543 ]
\ No newline at end of file
diff --git a/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.xml b/tests/Fixtures/Parsers/Bounds/bounds.xml
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/BoundsParserTest.xml
rename to tests/Fixtures/Parsers/Bounds/bounds.xml
diff --git a/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.json b/tests/Fixtures/Parsers/Copyright/copyright.json
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.json
rename to tests/Fixtures/Parsers/Copyright/copyright.json
diff --git a/tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.xml b/tests/Fixtures/Parsers/Copyright/copyright.xml
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/CopyrightParserTest.xml
rename to tests/Fixtures/Parsers/Copyright/copyright.xml
diff --git a/tests/UnitTests/phpGPX/Parsers/EmailParserTest.json b/tests/Fixtures/Parsers/Email/email.json
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/EmailParserTest.json
rename to tests/Fixtures/Parsers/Email/email.json
diff --git a/tests/UnitTests/phpGPX/Parsers/EmailParserTest.xml b/tests/Fixtures/Parsers/Email/email.xml
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/EmailParserTest.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
new file mode 100644
index 0000000..1f809ce
--- /dev/null
+++ b/tests/Fixtures/Parsers/Extension/extension.json
@@ -0,0 +1,6 @@
+{
+ "trackPointExtension": {
+ "aTemp": 14,
+ "hr": 152
+ }
+}
\ No newline at end of file
diff --git a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.xml b/tests/Fixtures/Parsers/Extension/extension.xml
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.xml
rename to tests/Fixtures/Parsers/Extension/extension.xml
diff --git a/tests/UnitTests/phpGPX/Parsers/LinkParserTest.json b/tests/Fixtures/Parsers/Link/link.json
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/LinkParserTest.json
rename to tests/Fixtures/Parsers/Link/link.json
diff --git a/tests/UnitTests/phpGPX/Parsers/LinkParserTest.xml b/tests/Fixtures/Parsers/Link/link.xml
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/LinkParserTest.xml
rename to tests/Fixtures/Parsers/Link/link.xml
diff --git a/tests/UnitTests/phpGPX/Parsers/PersonParserTest.json b/tests/Fixtures/Parsers/Person/person.json
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/PersonParserTest.json
rename to tests/Fixtures/Parsers/Person/person.json
diff --git a/tests/UnitTests/phpGPX/Parsers/PersonParserTest.xml b/tests/Fixtures/Parsers/Person/person.xml
similarity index 100%
rename from tests/UnitTests/phpGPX/Parsers/PersonParserTest.xml
rename to tests/Fixtures/Parsers/Person/person.xml
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/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
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/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
diff --git a/tests/Integration/GeoJsonOutputTest.php b/tests/Integration/GeoJsonOutputTest.php
new file mode 100644
index 0000000..c676441
--- /dev/null
+++ b/tests/Integration/GeoJsonOutputTest.php
@@ -0,0 +1,168 @@
+gpx = new phpGPX();
+ }
+
+ public function testGpxFileJsonSerializeIsFeatureCollection(): void
+ {
+ $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx');
+ $json = $gpxFile->jsonSerialize();
+
+ $this->assertEquals('FeatureCollection', $json['type']);
+ $this->assertArrayHasKey('features', $json);
+ $this->assertIsArray($json['features']);
+ }
+
+ public function testWaypointJsonIsPointFeature(): void
+ {
+ $point = new Point(PointType::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(PointType::Routepoint);
+ $p1->latitude = 54.932;
+ $p1->longitude = 9.860;
+ $p1->elevation = 0.0;
+
+ $p2 = new Point(PointType::Routepoint);
+ $p2->latitude = 54.933;
+ $p2->longitude = 9.861;
+ $p2->elevation = 1.0;
+
+ $route->points = [$p1, $p2];
+
+ $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(PointType::Trackpoint);
+ $p1->latitude = 46.571;
+ $p1->longitude = 8.414;
+ $p1->elevation = 2419.0;
+
+ $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(PointType::Trackpoint);
+ $p3->latitude = 46.573;
+ $p3->longitude = 8.416;
+ $p3->elevation = 2421.0;
+ $seg2->points = [$p3];
+
+ $track->segments = [$seg1, $seg2];
+
+ $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 = $this->gpx->load(self::FIXTURES_DIR . '/minimal.gpx');
+ $json = json_decode(json_encode($gpxFile), true);
+
+ $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 = $this->gpx->load(self::FIXTURES_DIR . '/timezero.gpx');
+ $json = json_decode(json_encode($gpxFile), true);
+
+ $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 = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx');
+
+ $geoJson = $gpxFile->toJSON();
+ $decoded = json_decode($geoJson, true);
+ $this->assertNotNull($decoded);
+ $this->assertEquals('FeatureCollection', $decoded['type']);
+ $this->assertArrayHasKey('features', $decoded);
+ }
+}
diff --git a/tests/Integration/GpxFileLoadTest.php b/tests/Integration/GpxFileLoadTest.php
new file mode 100644
index 0000000..57b3ed2
--- /dev/null
+++ b/tests/Integration/GpxFileLoadTest.php
@@ -0,0 +1,166 @@
+gpx = new phpGPX(engine: Engine::default());
+ }
+
+ public function testLoadTimezeroGpx(): void
+ {
+ $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/timezero.gpx');
+
+ // Waypoints
+ $this->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 = $this->gpx->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 = $this->gpx->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 = $this->gpx->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->get(\phpGPX\Models\Extensions\TrackPointExtension::class));
+ $this->assertEqualsWithDelta(126, $firstPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr, 0.1);
+ }
+
+ public function testLoadCreatorAttribute(): void
+ {
+ $gpxFile = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx');
+ $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');
+ $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
new file mode 100644
index 0000000..d166f25
--- /dev/null
+++ b/tests/Integration/XmlRoundTripTest.php
@@ -0,0 +1,155 @@
+gpx = new phpGPX(engine: Engine::default());
+ }
+
+ /**
+ * Load a GPX file, serialize to XML, parse again, and verify key data is preserved.
+ */
+ public function testRoundTripTimezero(): void
+ {
+ $original = $this->gpx->load(self::FIXTURES_DIR . '/timezero.gpx');
+ $xml = $original->toXML()->saveXML();
+ $reloaded = $this->gpx->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 = $this->gpx->load(self::FIXTURES_DIR . '/route.gpx');
+ $xml = $original->toXML()->saveXML();
+ $reloaded = $this->gpx->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 = $this->gpx->load(self::FIXTURES_DIR . '/gps-track.gpx');
+ $xml = $original->toXML()->saveXML();
+ $reloaded = $this->gpx->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 = $this->gpx->load(self::FIXTURES_DIR . '/minimal.gpx');
+ $xml = $original->toXML()->saveXML();
+ $reloaded = $this->gpx->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->get(\phpGPX\Models\Extensions\TrackPointExtension::class));
+ $this->assertEqualsWithDelta(
+ $origPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr,
+ $reloadedPoint->extensions->get(\phpGPX\Models\Extensions\TrackPointExtension::class)->hr,
+ 0.1,
+ );
+ }
+
+ public function testRoundTripStatsConsistency(): void
+ {
+ $original = $this->gpx->load(self::FIXTURES_DIR . '/gps-track.gpx');
+ $xml = $original->toXML()->saveXML();
+ $reloaded = $this->gpx->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,
+ );
+ }
+}
diff --git a/tests/LoadFileTest.php b/tests/LoadFileTest.php
deleted file mode 100644
index c419664..0000000
--- a/tests/LoadFileTest.php
+++ /dev/null
@@ -1,224 +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/tests/LoadRouteFileTest.php b/tests/LoadRouteFileTest.php
deleted file mode 100644
index 71b370a..0000000
--- a/tests/LoadRouteFileTest.php
+++ /dev/null
@@ -1,182 +0,0 @@
-
- */
-
-
-namespace phpGPX\Tests;
-
-use phpGPX\phpGPX;
-use PHPUnit\Framework\TestCase;
-
-class LoadRouteFileTest extends TestCase
-{
- 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();
- }
-
- 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/tests/Unit/Analysis/BoundsAnalyzerTest.php b/tests/Unit/Analysis/BoundsAnalyzerTest.php
new file mode 100644
index 0000000..6aefb32
--- /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(PointType::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(PointType::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);
+ }
+}
diff --git a/tests/Unit/Analysis/EngineTest.php b/tests/Unit/Analysis/EngineTest.php
new file mode 100644
index 0000000..22facc8
--- /dev/null
+++ b/tests/Unit/Analysis/EngineTest.php
@@ -0,0 +1,341 @@
+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(PointType::Routepoint);
+ $p1->latitude = 48.002;
+ $p1->longitude = 17.0;
+ $p1->time = new \DateTime('2024-01-01T10:00:20Z');
+
+ $p2 = new Point(PointType::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);
+ }
+}
diff --git a/tests/Unit/Analysis/MovementAnalyzerTest.php b/tests/Unit/Analysis/MovementAnalyzerTest.php
new file mode 100644
index 0000000..43a7e6b
--- /dev/null
+++ b/tests/Unit/Analysis/MovementAnalyzerTest.php
@@ -0,0 +1,179 @@
+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(PointType::Trackpoint);
+ $p1->latitude = 48.0;
+ $p1->longitude = 17.0;
+
+ $p2 = new Point(PointType::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);
+ }
+}
diff --git a/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php
new file mode 100644
index 0000000..5f12f6c
--- /dev/null
+++ b/tests/Unit/Analysis/TrackPointExtensionAnalyzerTest.php
@@ -0,0 +1,175 @@
+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(PointType::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->set($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);
+ }
+}
diff --git a/tests/Unit/Helpers/DateTimeHelperTest.php b/tests/Unit/Helpers/DateTimeHelperTest.php
new file mode 100644
index 0000000..f2a8cf8
--- /dev/null
+++ b/tests/Unit/Helpers/DateTimeHelperTest.php
@@ -0,0 +1,42 @@
+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');
+ }
+}
diff --git a/tests/Unit/Helpers/DistanceCalculatorTest.php b/tests/Unit/Helpers/DistanceCalculatorTest.php
new file mode 100644
index 0000000..2fee611
--- /dev/null
+++ b/tests/Unit/Helpers/DistanceCalculatorTest.php
@@ -0,0 +1,122 @@
+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
+ {
+ // 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], applySmoothing: true, smoothingThreshold: 10);
+ $distance = $calc->getRawDistance();
+
+ // With smoothing, these tiny movements should be filtered out
+ $this->assertEqualsWithDelta(0.0, $distance, 0.01);
+ }
+
+ public function testDistanceSmoothingKeepsLargeMovements(): void
+ {
+ // 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], applySmoothing: true, smoothingThreshold: 2);
+ $distance = $calc->getRawDistance();
+
+ $this->assertGreaterThan(800, $distance);
+ }
+
+ 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);
+ }
+}
diff --git a/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php
new file mode 100644
index 0000000..8a34694
--- /dev/null
+++ b/tests/Unit/Helpers/ElevationGainLossCalculatorTest.php
@@ -0,0 +1,187 @@
+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(PointType::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
+ {
+ $points = [
+ $this->makePoint(100),
+ $this->makePoint(0), // should be skipped
+ $this->makePoint(200),
+ ];
+
+ [$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
+ {
+ $points = [
+ $this->makePoint(100),
+ $this->makePoint(0),
+ $this->makePoint(200),
+ ];
+
+ [$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
+ {
+ // 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,
+ applySmoothing: true,
+ smoothingThreshold: 5,
+ );
+ $this->assertEqualsWithDelta(0.0, $gain, 0.001);
+ $this->assertEqualsWithDelta(0.0, $loss, 0.001);
+ }
+
+ public function testSmoothingKeepsLargeChanges(): void
+ {
+ // Large change of 50m — above 5m threshold
+ $points = [
+ $this->makePoint(100),
+ $this->makePoint(150),
+ ];
+
+ [$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
+ {
+ // 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,
+ 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
+ $this->assertEqualsWithDelta(5.0, $gain, 0.001);
+ $this->assertEqualsWithDelta(0.0, $loss, 0.001);
+ }
+}
diff --git a/tests/Unit/Helpers/GeoHelperTest.php b/tests/Unit/Helpers/GeoHelperTest.php
new file mode 100644
index 0000000..43d9457
--- /dev/null
+++ b/tests/Unit/Helpers/GeoHelperTest.php
@@ -0,0 +1,95 @@
+latitude = 48.1573923225717;
+ $point1->longitude = 17.0547121910204;
+
+ $point2 = new Point(PointType::Waypoint);
+ $point2->latitude = 48.1644916381763;
+ $point2->longitude = 17.0591753907502;
+
+ $this->assertEqualsWithDelta(
+ 856.97,
+ GeoHelper::getRawDistance($point1, $point2),
+ 1,
+ 'Invalid distance between two points!',
+ );
+ }
+
+ /**
+ * @link http://cosinekitty.com/compass.html
+ */
+ public function testRealDistance(): void
+ {
+ $point1 = new Point(PointType::Waypoint);
+ $point1->latitude = 48.1573923225717;
+ $point1->longitude = 17.0547121910204;
+ $point1->elevation = 100;
+
+ $point2 = new Point(PointType::Waypoint);
+ $point2->latitude = 48.1644916381763;
+ $point2->longitude = 17.0591753907502;
+ $point2->elevation = 200;
+
+ $this->assertEqualsWithDelta(
+ 856.97,
+ GeoHelper::getRawDistance($point1, $point2),
+ 1,
+ 'Invalid distance between two points!',
+ );
+
+ $this->assertEqualsWithDelta(
+ 862,
+ GeoHelper::getRealDistance($point1, $point2),
+ 1,
+ 'Invalid real distance between two points!',
+ );
+ }
+
+ public function testSamePointZeroDistance(): void
+ {
+ $point1 = new Point(PointType::Waypoint);
+ $point1->latitude = 48.1573923225717;
+ $point1->longitude = 17.0547121910204;
+
+ $point2 = new Point(PointType::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(PointType::Waypoint);
+ $point1->latitude = 48.1573923225717;
+ $point1->longitude = 17.0547121910204;
+
+ $point2 = new Point(PointType::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);
+ }
+}
diff --git a/tests/Unit/Helpers/SerializationHelperTest.php b/tests/Unit/Helpers/SerializationHelperTest.php
new file mode 100644
index 0000000..f398b98
--- /dev/null
+++ b/tests/Unit/Helpers/SerializationHelperTest.php
@@ -0,0 +1,27 @@
+assertEquals([9.860, 54.932, 100.5], $pos);
+ }
+
+ public function testPositionWithoutElevation(): void
+ {
+ $pos = SerializationHelper::position(9.860, 54.932);
+ $this->assertEquals([9.860, 54.932], $pos);
+ }
+
+ public function testPositionWithNullElevation(): void
+ {
+ $pos = SerializationHelper::position(9.860, 54.932, null);
+ $this->assertEquals([9.860, 54.932], $pos);
+ }
+}
diff --git a/tests/Unit/Models/BoundsTest.php b/tests/Unit/Models/BoundsTest.php
new file mode 100644
index 0000000..8f9fed1
--- /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);
+ }
+}
diff --git a/tests/Unit/Models/StatsCalculationTest.php b/tests/Unit/Models/StatsCalculationTest.php
new file mode 100644
index 0000000..938ee5a
--- /dev/null
+++ b/tests/Unit/Models/StatsCalculationTest.php
@@ -0,0 +1,372 @@
+engine = (new Engine())
+ ->addAnalyzer(new DistanceAnalyzer())
+ ->addAnalyzer(new ElevationAnalyzer())
+ ->addAnalyzer(new AltitudeAnalyzer())
+ ->addAnalyzer(new TimestampAnalyzer());
+ }
+
+ private function makePoint(
+ float $lat,
+ float $lon,
+ ?float $ele = null,
+ ?string $time = null,
+ ): Point {
+ $p = new Point(PointType::Trackpoint);
+ $p->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(PointType::Routepoint);
+ $p->latitude = $lat;
+ $p->longitude = $lon;
+ $p->elevation = $ele;
+ $p->time = $time ? new \DateTime($time) : null;
+ 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();
+ $result = $this->processSegment($segment);
+
+ $stats = $result->tracks[0]->segments[0]->stats;
+ $this->assertInstanceOf(Stats::class, $stats);
+ $this->assertNull($stats->distance);
+ }
+
+ public function testSegmentStatsSinglePoint(): void
+ {
+ $segment = new Segment();
+ $segment->points = [
+ $this->makePoint(46.571948, 8.414757, 2419, '2017-08-13T07:10:41Z'),
+ ];
+ $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
+ {
+ $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'),
+ ];
+ $result = $this->processSegment($segment);
+ $stats = $result->tracks[0]->segments[0]->stats;
+
+ // Distance should be positive
+ $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, $stats->cumulativeElevationGain);
+
+ // Elevation loss: 2419→2418.88 (-0.12)
+ $this->assertGreaterThan(0, $stats->cumulativeElevationLoss);
+
+ // Duration
+ $this->assertEqualsWithDelta(97.0, $stats->duration, 0.1);
+
+ // Speed and pace
+ $this->assertNotNull($stats->averageSpeed);
+ $this->assertGreaterThan(0, $stats->averageSpeed);
+ $this->assertNotNull($stats->averagePace);
+ $this->assertGreaterThan(0, $stats->averagePace);
+
+ // Altitude bounds
+ $this->assertEqualsWithDelta(2418.88, $stats->minAltitude, 0.01);
+ $this->assertEqualsWithDelta(2425, $stats->maxAltitude, 0.01);
+
+ // Start/end coordinates
+ $this->assertEquals(46.571948, $stats->startedAtCoords['lat']);
+ $this->assertEquals(46.572054, $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),
+ ];
+ $result = $this->processSegment($segment);
+ $stats = $result->tracks[0]->segments[0]->stats;
+
+ // Distance should still be calculated
+ $this->assertGreaterThan(0, $stats->distance);
+
+ // Duration, speed, pace should be null (no timestamps)
+ $this->assertNull($stats->duration);
+ $this->assertNull($stats->averageSpeed);
+ $this->assertNull($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'),
+ ];
+ $result = $this->processSegment($segment);
+ $stats = $result->tracks[0]->segments[0]->stats;
+
+ $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
+ {
+ $engine = (new Engine())
+ ->addAnalyzer(new DistanceAnalyzer())
+ ->addAnalyzer(new ElevationAnalyzer(ignoreZeroElevation: true))
+ ->addAnalyzer(new AltitudeAnalyzer(ignoreZeroElevation: true))
+ ->addAnalyzer(new TimestampAnalyzer());
+
+ $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'),
+ ];
+
+ $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, $result->tracks[0]->segments[0]->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'),
+ ];
+ $result = $this->processSegment($segment);
+ $firstDistance = $result->tracks[0]->segments[0]->stats->distance;
+
+ // 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 ---
+
+ public function testTrackStatsEmptySegments(): void
+ {
+ $track = new Track();
+ $result = $this->processTrack($track);
+
+ $this->assertInstanceOf(Stats::class, $result->tracks[0]->stats);
+ $this->assertNull($result->tracks[0]->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];
+ $result = $this->processTrack($track);
+ $stats = $result->tracks[0]->stats;
+
+ $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
+ {
+ $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];
+ $result = $this->processTrack($track);
+ $stats = $result->tracks[0]->stats;
+
+ // 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, $stats->cumulativeElevationGain, 0.01);
+
+ // Elevation loss aggregated: seg1 has 0, seg2 has 20m loss
+ $this->assertEqualsWithDelta(20.0, $stats->cumulativeElevationLoss, 0.01);
+
+ // Min altitude should be minimum across all segments
+ $this->assertEqualsWithDelta(100.0, $stats->minAltitude, 0.01);
+
+ // Max altitude should be maximum across all segments
+ $this->assertEqualsWithDelta(200.0, $stats->maxAltitude, 0.01);
+
+ // Start/end should span the entire track
+ $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, $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();
+ $result = $this->processRoute($route);
+
+ $this->assertInstanceOf(Stats::class, $result->routes[0]->stats);
+ $this->assertNull($result->routes[0]->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),
+ ];
+ $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 ---
+
+ public function testStatsJsonSerialize(): 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;
+
+ $json = $stats->jsonSerialize();
+
+ $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']);
+ }
+}
diff --git a/tests/Unit/Parsers/BoundsParserTest.php b/tests/Unit/Parsers/BoundsParserTest.php
new file mode 100644
index 0000000..31115b2
--- /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());
+ }
+}
diff --git a/tests/Unit/Parsers/CopyrightParserTest.php b/tests/Unit/Parsers/CopyrightParserTest.php
new file mode 100644
index 0000000..1f66e39
--- /dev/null
+++ b/tests/Unit/Parsers/CopyrightParserTest.php
@@ -0,0 +1,59 @@
+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->jsonSerialize(), $copyright->jsonSerialize());
+ }
+
+ 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->jsonSerialize()),
+ );
+ }
+}
diff --git a/tests/Unit/Parsers/EmailParserTest.php b/tests/Unit/Parsers/EmailParserTest.php
new file mode 100644
index 0000000..57edb9a
--- /dev/null
+++ b/tests/Unit/Parsers/EmailParserTest.php
@@ -0,0 +1,57 @@
+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->jsonSerialize(), $email->jsonSerialize());
+ }
+
+ 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->jsonSerialize()),
+ );
+ }
+}
diff --git a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php b/tests/Unit/Parsers/ExtensionParserTest.php
similarity index 57%
rename from tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php
rename to tests/Unit/Parsers/ExtensionParserTest.php
index 54f67dc..217f23e 100644
--- a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.php
+++ b/tests/Unit/Parsers/ExtensionParserTest.php
@@ -1,72 +1,58 @@
aTemp = (float) 14;
- $trackpoint->avgTemperature = (float) 14;
- $trackpoint->hr = (float) 152;
- $trackpoint->heartRate = (float) 152;
+ $trackpoint->aTemp = 14.0;
+ $trackpoint->hr = 152.0;
- $extensions = new Extensions();
- $extensions->trackPointExtension = $trackpoint;
+ $this->extensions = new Extensions();
+ $this->extensions->set($trackpoint);
- return $extensions;
- }
-
- protected function setUp(): void
- {
- parent::setUp();
+ $this->file = simplexml_load_file(self::FIXTURES_DIR . '/extension.xml');
- $this->testModelInstance = self::createTestInstance();
+ // Configure the registry for parsing
+ ExtensionParser::$registry = ExtensionRegistry::default();
}
- public function testParse()
+ public function testParse(): void
{
- $extensions = ExtensionParser::parse($this->testXmlFile->extensions);
+ $extensions = ExtensionParser::parse($this->file->extensions);
- $this->assertEquals($this->testModelInstance->unsupported, $extensions->unsupported);
- $this->assertEquals($this->testModelInstance->trackPointExtension, $extensions->trackPointExtension);
-
- $this->assertEquals($this->testModelInstance->toArray(), $extensions->toArray());
- }
+ $this->assertEquals($this->extensions->unsupported, $extensions->unsupported);
+ $parsed = $extensions->get(TrackPointExtension::class);
+ $expected = $this->extensions->get(TrackPointExtension::class);
+ $this->assertNotNull($parsed);
+ $this->assertEquals($expected->jsonSerialize(), $parsed->jsonSerialize());
- /**
- * Returns output of ::toXML method of tested parser.
- * @param \DOMDocument $document
- * @return \DOMElement
- */
- protected function convertToXML(\DOMDocument $document)
- {
- return ExtensionParser::toXML($this->testModelInstance, $document);
+ $this->assertJsonStringEqualsJsonString(
+ json_encode($this->extensions),
+ json_encode($extensions),
+ );
}
- public function testToXML()
+ public function testToXML(): void
{
- $document = new \DOMDocument("1.0", 'UTF-8');
+ $document = new \DOMDocument('1.0', 'UTF-8');
- $root = $document->createElement("document");
- $root->appendChild($this->convertToXML($document));
+ $root = $document->createElement('document');
+ $root->appendChild(ExtensionParser::toXML($this->extensions, $document));
$attributes = [
'xmlns' => 'http://www.topografix.com/GPX/1/1',
@@ -84,6 +70,14 @@ public function testToXML()
$document->appendChild($root);
- $this->assertXmlStringEqualsXmlString($this->testXmlFile->asXML(), $document->saveXML());
+ $this->assertXmlStringEqualsXmlString($this->file->asXML(), $document->saveXML());
+ }
+
+ public function testToJSON(): void
+ {
+ $this->assertJsonStringEqualsJsonFile(
+ self::FIXTURES_DIR . '/extension.json',
+ json_encode($this->extensions->jsonSerialize()),
+ );
}
}
diff --git a/tests/Unit/Parsers/ExtensionRegistryTest.php b/tests/Unit/Parsers/ExtensionRegistryTest.php
new file mode 100644
index 0000000..eee3142
--- /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'));
+ }
+}
diff --git a/tests/Unit/Parsers/LinkParserTest.php b/tests/Unit/Parsers/LinkParserTest.php
new file mode 100644
index 0000000..62d5f95
--- /dev/null
+++ b/tests/Unit/Parsers/LinkParserTest.php
@@ -0,0 +1,57 @@
+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
+ {
+ $link = LinkParser::parse($this->file->link);
+
+ $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);
+
+ $this->assertEquals($this->link->jsonSerialize(), $link->jsonSerialize());
+ }
+
+ public function testToXML(): void
+ {
+ $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());
+ }
+
+ public function testToJSON(): void
+ {
+ $this->assertJsonStringEqualsJsonFile(
+ self::FIXTURES_DIR . '/link.json',
+ json_encode($this->link->jsonSerialize()),
+ );
+ }
+}
diff --git a/tests/Unit/Parsers/PersonParserTest.php b/tests/Unit/Parsers/PersonParserTest.php
new file mode 100644
index 0000000..f7d1c6f
--- /dev/null
+++ b/tests/Unit/Parsers/PersonParserTest.php
@@ -0,0 +1,93 @@
+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->jsonSerialize(), $person->jsonSerialize());
+ $this->assertEquals($this->person->email->jsonSerialize(), $person->email->jsonSerialize());
+ $this->assertEquals($this->person->links[0]->jsonSerialize(), $person->links[0]->jsonSerialize());
+ }
+
+ /**
+ * @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->jsonSerialize()),
+ );
+ }
+}
diff --git a/tests/UnitTests/phpGPX/Helpers/DateTimeHelperTest.php b/tests/UnitTests/phpGPX/Helpers/DateTimeHelperTest.php
deleted file mode 100644
index 2374ce5..0000000
--- a/tests/UnitTests/phpGPX/Helpers/DateTimeHelperTest.php
+++ /dev/null
@@ -1,72 +0,0 @@
-
- */
-
-namespace UnitTests\phpGPX\Helpers;
-
-use phpGPX\Helpers\DateTimeHelper;
-use phpGPX\Models\Point;
-use PHPUnit\Framework\TestCase;
-
-class DateTimeHelperTest extends TestCase
-{
- 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));
- }
-
- 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')
- );
- }
-
- 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")
- );
- }
-
- public function testParseDateTimeInvalidInput()
- {
- $this->expectException("Exception");
- DateTimeHelper::parseDateTime("Invalid exception");
- }
-}
diff --git a/tests/UnitTests/phpGPX/Helpers/GeoHelperTest.php b/tests/UnitTests/phpGPX/Helpers/GeoHelperTest.php
deleted file mode 100644
index 84e8269..0000000
--- a/tests/UnitTests/phpGPX/Helpers/GeoHelperTest.php
+++ /dev/null
@@ -1,69 +0,0 @@
-
- */
-
-namespace UnitTests\phpGPX\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
- */
- public function testGetDistance()
- {
- $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;
-
- $this->assertEqualsWithDelta(
- 856.97,
- GeoHelper::getRawDistance($point1, $point2),
- 1,
- "Invalid distance between two points!"
- );
- }
-
- /**
- * @link http://cosinekitty.com/compass.html
- */
- public function testRealDistance()
- {
- $point1 = new Point(Point::WAYPOINT);
- $point1->latitude = 48.1573923225717;
- $point1->longitude = 17.0547121910204;
- $point1->elevation = 100;
-
- $point2 = new Point(Point::WAYPOINT);
- $point2->latitude = 48.1644916381763;
- $point2->longitude = 17.0591753907502;
- $point2->elevation = 200;
-
- $this->assertEqualsWithDelta(
- 856.97,
- GeoHelper::getRawDistance($point1, $point2),
- 1,
- "Invalid distance between two points!"
- );
-
- $this->assertEqualsWithDelta(
- 862,
- GeoHelper::getRealDistance($point1, $point2),
- 1,
- "Invalid real distance between two points!"
- );
- }
-}
diff --git a/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php b/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php
deleted file mode 100644
index b214fb3..0000000
--- a/tests/UnitTests/phpGPX/Helpers/SerializationHelperTest.php
+++ /dev/null
@@ -1,93 +0,0 @@
-
- */
-
-namespace UnitTests\phpGPX\Helpers;
-
-use phpGPX\Helpers\SerializationHelper;
-use PHPUnit\Framework\TestCase;
-
-class SerializationHelperTest extends TestCase
-{
- 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"));
- }
-
- 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"));
- }
-
- public function testStringOrNull()
- {
- $this->assertNull(SerializationHelper::stringOrNull(null));
- $this->assertIsString(SerializationHelper::stringOrNull(""));
- $this->assertIsString(SerializationHelper::stringOrNull("Bla bla"));
- }
-
- /**
- * @dataProvider dataProviderFilterNotNull
- */
- 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],
- ],
- ];
- }
-}
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.json b/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.json
deleted file mode 100644
index 2b529a2..0000000
--- a/tests/UnitTests/phpGPX/Parsers/BoundsParserTest.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "maxlat": 49.090543,
- "maxlon": 18.886939,
- "minlat": 49.072489,
- "minlon": 18.814543
-}
\ No newline at end of file
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/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);
- }
-}
diff --git a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.json b/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.json
deleted file mode 100644
index 4cbd70b..0000000
--- a/tests/UnitTests/phpGPX/Parsers/ExtensionParserTest.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "trackpoint": {
- "aTemp": 14,
- "wTemp": null,
- "depth": null,
- "hr": 152,
- "cad": null,
- "speed": null,
- "course": null,
- "bearing": null
- },
- "unsupported": []
-}
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());
- }
-}