├── .gitignore
├── tests
├── Resources
│ └── Gtfs
│ │ └── minified-test-feed.zip
└── Trafiklab
│ └── Gtfs
│ └── GtfsArchiveIntegrationTest.php
├── phpcs.xml
├── .travis.yml
├── src
└── Trafiklab
│ └── Gtfs
│ ├── Model
│ ├── Files
│ │ ├── GtfsFeedInfoFile.php
│ │ ├── GtfsTransfersFile.php
│ │ ├── GtfsCalendarFile.php
│ │ ├── GtfsAgencyFile.php
│ │ ├── GtfsFrequenciesFile.php
│ │ ├── GtfsTripsFile.php
│ │ ├── GtfsStopsFile.php
│ │ ├── GtfsRoutesFile.php
│ │ ├── GtfsCalendarDatesFile.php
│ │ ├── GtfsShapesFile.php
│ │ └── GtfsStopTimesFile.php
│ ├── Entities
│ │ ├── CalendarDate.php
│ │ ├── CalendarEntry.php
│ │ ├── Frequency.php
│ │ ├── Transfer.php
│ │ ├── ShapePoint.php
│ │ ├── Agency.php
│ │ ├── FeedInfo.php
│ │ ├── Trip.php
│ │ ├── Route.php
│ │ ├── StopTime.php
│ │ └── Stop.php
│ ├── GtfsRouteType.php
│ └── GtfsArchive.php
│ └── Util
│ └── Internal
│ ├── ArrayCache.php
│ ├── BinarySearchUtil.php
│ └── GtfsParserUtil.php
├── phpunit.xml
├── composer.json
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | vendor/
3 | .idea
4 | composer.lock
5 | .testkeys
6 | .cache
7 | .idea
8 | .DS_Store
9 |
--------------------------------------------------------------------------------
/tests/Resources/Gtfs/minified-test-feed.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trafiklab/gtfs-php-sdk/HEAD/tests/Resources/Gtfs/minified-test-feed.zip
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - '7.3'
4 | - '8.0'
5 | install: composer install
6 |
7 | # Testing the app (see phpunit.xml) for configs, generating Code Coverage report
8 | script:
9 | - composer test -- --coverage-clover=coverage.xml
10 |
11 | env:
12 | global:
13 | - LIMIT_MEMORY_USAGE=true
14 |
15 | after_success:
16 | - bash <(curl -s https://codecov.io/bash)
17 |
18 | # You can delete the cache using travis-ci web interface
19 | cache:
20 | directories:
21 | - $HOME/.composer/cache
22 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsFeedInfoFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
17 | FeedInfo::class);
18 | }
19 |
20 | /**
21 | * Get the file data as an array of its rows.
22 | *
23 | * @return FeedInfo[]
24 | */
25 | public function getFeedInfo(): array
26 | {
27 | return $this->dataRows;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsTransfersFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
17 | Transfer::class);
18 | }
19 |
20 | /**
21 | * Get the file data as an array of its rows.
22 | *
23 | * @return Transfer[]
24 | */
25 | public function getTransfers(): array
26 | {
27 | return $this->dataRows;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsCalendarFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
17 | CalendarEntry::class);
18 | }
19 |
20 | /**
21 | * Get the file data as an array of its rows.
22 | *
23 | * @return CalendarEntry[]
24 | */
25 | public function getCalendarEntries(): array
26 | {
27 | return $this->dataRows;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsAgencyFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV(
17 | $parent,
18 | $filePath,
19 | Agency::class
20 | );
21 | }
22 |
23 | /**
24 | * Get the file data as an array of its rows.
25 | *
26 | * @return Agency[]
27 | */
28 | public function getAgencies(): array
29 | {
30 | return $this->dataRows;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsFrequenciesFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV(
17 | $parent,
18 | $filePath,
19 | Frequency::class
20 | );
21 | }
22 |
23 | /**
24 | * Get the file data as an array of its rows.
25 | *
26 | * @return Frequency[]
27 | */
28 | public function getFrequencies(): array
29 | {
30 | return $this->dataRows;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | ./src/Trafiklab
16 |
17 |
18 |
19 |
20 | ./tests/
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "trafiklab/gtfs-php-sdk",
3 | "description": "Use GTFS and GTFS-realtime data in your PHP projects",
4 | "type": "library",
5 | "license": "MPL-2.0",
6 | "authors": [
7 | {
8 | "name": "Bert Marcelis",
9 | "email": "bert.marcelis@samtrafiken.se"
10 | }
11 | ],
12 | "keywords": [
13 | "trafiklab",
14 | "php",
15 | "sdk",
16 | "gtfs",
17 | "gtfs-rt",
18 | "public transport"
19 | ],
20 | "homepage": "https://trafiklab.se",
21 | "require": {
22 | "php": "^7.2 || ^8.0",
23 | "ext-json": "*",
24 | "ext-zip": "*"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^9.0",
28 | "squizlabs/php_codesniffer": "^3.4",
29 | "phpmd/phpmd": "^2.6.0",
30 | "friendsofphp/php-cs-fixer": "^2.14"
31 | },
32 | "scripts": {
33 | "test": "phpunit"
34 | },
35 | "autoload": {
36 | "psr-0": {
37 | "Trafiklab\\Gtfs": "src/"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Util/Internal/ArrayCache.php:
--------------------------------------------------------------------------------
1 | _cache)) {
21 | return null;
22 | }
23 | return $this->_cache[$method];
24 | }
25 |
26 | /**
27 | * Cache data to ensure a method call doesn't do work twice.
28 | *
29 | * @param string $method The method which generated the data.
30 | * @param mixed $value The value to cache.
31 | */
32 | private function setCachedResult(string $method, $value)
33 | {
34 | $this->_cache[$method] = $value;
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsTripsFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
17 | Trip::class, 'trip_id');
18 | }
19 |
20 | /**
21 | * Get the file data as an array of its rows.
22 | *
23 | * @return Trip[]
24 | */
25 | public function getTrips(): array
26 | {
27 | return $this->dataRows;
28 | }
29 |
30 | /**
31 | * @param string $tripId
32 | *
33 | * @return null|Trip
34 | */
35 | public function getTrip(string $tripId): ?Trip
36 | {
37 | $trips = $this->getTrips();
38 | if (!key_exists($tripId, $trips)) {
39 | return null;
40 | }
41 | return $trips[$tripId];
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsStopsFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
18 | Stop::class, 'stop_id');
19 | }
20 |
21 | /**
22 | * Get the file data as an array of its rows.
23 | *
24 | * @return Stop[]
25 | */
26 | public function getStops(): array
27 | {
28 | return $this->dataRows;
29 | }
30 |
31 | /**
32 | * @param string $stopId
33 | *
34 | * @return null|Stop
35 | */
36 | public function getStop(string $stopId): ?Stop
37 | {
38 | $stops = $this->getStops();
39 | if (!key_exists($stopId, $stops)) {
40 | return null;
41 | }
42 | return $stops[$stopId];
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsRoutesFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
17 | Route::class, 'route_id');
18 | }
19 |
20 | /**
21 | * Get the file data as an array of its rows.
22 | *
23 | * @return Route[]
24 | */
25 | public function getRoutes(): array
26 | {
27 | return $this->dataRows;
28 | }
29 |
30 | /**
31 | * @param string $routeId
32 | *
33 | * @return Route | null
34 | */
35 | public function getRoute(string $routeId): ?Route
36 | {
37 | $routes = $this->getRoutes();
38 | if (!key_exists($routeId, $routes)) {
39 | return null;
40 | }
41 | return $routes[$routeId];
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsCalendarDatesFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath,
17 | CalendarDate::class);
18 | }
19 |
20 | /**
21 | * Get the file data as an array of its rows.
22 | *
23 | * @return CalendarDate[]
24 | */
25 | public function getCalendarDates(): array
26 | {
27 | return $this->dataRows;
28 | }
29 |
30 | /**
31 | * Get all the calendar dates where service_id matches the given serviceId parameter.
32 | *
33 | * @param string $serviceId
34 | *
35 | * @return CalendarDate[]
36 | */
37 | public function getCalendarDatesForService(string $serviceId): array
38 | {
39 | $calendarDates = $this->getCalendarDates();
40 | $result = [];
41 | foreach ($calendarDates as $calendarDate) {
42 | if ($calendarDate->getServiceId() == $serviceId) {
43 | $result[] = $calendarDate;
44 | }
45 | }
46 | return $result;
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/CalendarDate.php:
--------------------------------------------------------------------------------
1 | $value) {
30 | $this->$variable = $value;
31 | }
32 | $this->archive = $archive;
33 | }
34 |
35 |
36 | /**
37 | * @return mixed
38 | */
39 | public function getDate() : DateTime
40 | {
41 | return DateTime::createFromFormat("Ymd", $this->date);
42 | }
43 |
44 | /**
45 | * @return mixed
46 | */
47 | public function getExceptionType()
48 | {
49 | return $this->exception_type;
50 | }
51 |
52 | /**
53 | * @return mixed
54 | */
55 | public function getServiceId()
56 | {
57 | return $this->service_id;
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsShapesFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSVWithCompositeIndex($parent, $filePath,
23 | ShapePoint::class, 'shape_id', 'shape_pt_sequence');
24 | $this->dataRows = array_values($this->dataRows);
25 | }
26 |
27 | /**
28 | * Get the file data as an array of its rows.
29 | *
30 | * @return ShapePoint[]
31 | */
32 | public function getShapePoints(): array
33 | {
34 | return $this->dataRows;
35 | }
36 |
37 |
38 | /**
39 | * @param string $shapeId
40 | *
41 | * @return ShapePoint[]
42 | */
43 | public function getShape(string $shapeId): array
44 | {
45 | if ($this->getCachedResult(__METHOD__ . "::" . $shapeId) != null) {
46 | return $this->getCachedResult(__METHOD__ . "::" . $shapeId);
47 | }
48 |
49 | $shapePoints = $this->getShapePoints();
50 | $i = BinarySearchUtil::findIndexOfFirstFieldOccurrence($shapePoints, 'getShapeId', $shapeId);
51 |
52 | // If the shape id is not present
53 | if ($i == -1) {
54 | return [];
55 | }
56 |
57 | $result = [];
58 | while ($i < count($shapePoints) && $shapePoints[$i]->getShapeId() == $shapeId) {
59 | $result[] = $shapePoints[$i];
60 | $i++;
61 | }
62 |
63 | $this->setCachedResult(__METHOD__ . "::" . $shapeId, $result);
64 | return $result;
65 | }
66 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Util/Internal/BinarySearchUtil.php:
--------------------------------------------------------------------------------
1 | $getter();
44 |
45 | if ($middle != $start && $middle != $stop) {
46 | if ($middleValue > $needle) {
47 | return self::doBinarySearch($array, $getter, $needle, $start, $middle);
48 | }
49 |
50 | if ($middleValue < $needle) {
51 | return self::doBinarySearch($array, $getter, $needle, $middle, $stop);
52 | }
53 | }
54 |
55 | if ($middleValue != $needle) {
56 | return -1;
57 | }
58 |
59 | // If the value equals, we just loop back to the first match
60 | while ($middle > 0 && $array[$middle - 1]->$getter() == $needle) {
61 | $middle--;
62 | }
63 | return $middle;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Files/GtfsStopTimesFile.php:
--------------------------------------------------------------------------------
1 | dataRows = GtfsParserUtil::deserializeCSV($parent, $filePath, StopTime::class);
20 |
21 | // Create an array which gives all stop_times for each stop.
22 | // This way we only need to loop once over all stop times, but memory usage will be a bit higher.
23 | foreach ($this->dataRows as $stopTime) {
24 | if (!key_exists($stopTime->getStopId(), $this->stopTimesByStop)) {
25 | $this->stopTimesByStop[$stopTime->getStopId()] = [];
26 | }
27 | $this->stopTimesByStop[$stopTime->getStopId()][] = $stopTime;
28 | }
29 | }
30 |
31 | /**
32 | * Get the file data as an array of its rows.
33 | *
34 | * @return StopTime[]
35 | */
36 | public function getStopTimes(): array
37 | {
38 | return $this->dataRows;
39 | }
40 |
41 | /**
42 | * @param string $stopId
43 | *
44 | * @return StopTime[]
45 | */
46 | public function getStopTimesForStop(string $stopId): array
47 | {
48 | // Before we run a computationally intensive loop, check the table with included stops first.
49 | if (!key_exists($stopId, $this->stopTimesByStop)) {
50 | return [];
51 | }
52 |
53 | return $this->stopTimesByStop[$stopId];
54 | }
55 |
56 | /**
57 | * @param string $tripId
58 | *
59 | * @return StopTime[]
60 | */
61 | public function getStopTimesForTrip(string $tripId): array
62 | {
63 | $result = [];
64 | $stopTimes = $this->getStopTimes();
65 | foreach ($stopTimes as $stopTime) {
66 | if ($stopTime->getTripId() == $tripId) {
67 | $result[] = $stopTime;
68 | }
69 | }
70 | return $result;
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/CalendarEntry.php:
--------------------------------------------------------------------------------
1 | $value) {
36 | $this->$variable = $value;
37 | }
38 | $this->archive = $archive;
39 | }
40 |
41 | /**
42 | * @return mixed
43 | */
44 | public function getServiceId()
45 | {
46 | return $this->service_id;
47 | }
48 |
49 | /**
50 | * @return int
51 | */
52 | public function getMonday()
53 | {
54 | return $this->monday;
55 | }
56 |
57 | /**
58 | * @return int
59 | */
60 | public function getTuesday()
61 | {
62 | return $this->tuesday;
63 | }
64 |
65 | /**
66 | * @return int
67 | */
68 | public function getWednesday()
69 | {
70 | return $this->wednesday;
71 | }
72 |
73 | /**
74 | * @return int
75 | */
76 | public function getThursday()
77 | {
78 | return $this->thursday;
79 | }
80 |
81 | /**
82 | * @return int
83 | */
84 | public function getFriday()
85 | {
86 | return $this->friday;
87 | }
88 |
89 | /**
90 | * @return int
91 | */
92 | public function getSaturday()
93 | {
94 | return $this->saturday;
95 | }
96 |
97 | /**
98 | * @return int
99 | */
100 | public function getSunday()
101 | {
102 | return $this->sunday;
103 | }
104 |
105 | /**
106 | * @return DateTime
107 | */
108 | public function getStartDate(): DateTime
109 | {
110 | return DateTime::createFromFormat("Ymd", $this->start_date);
111 | }
112 |
113 | /**
114 | * @return DateTime
115 | */
116 | public function getEndDate(): DateTime
117 | {
118 | return DateTime::createFromFormat("Ymd", $this->end_date);
119 | }
120 |
121 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/Frequency.php:
--------------------------------------------------------------------------------
1 | $value) {
30 | $this->$variable = isset($value) && $value !== '' ? $value : null;
31 | }
32 | $this->archive = $archive;
33 | }
34 |
35 | /**
36 | * Identifies a trip to which the specified headway of service applies.
37 | *
38 | * @return string
39 | */
40 | public function getTripId(): string
41 | {
42 | return $this->trip_id;
43 | }
44 |
45 | /**
46 | * Time at which the first vehicle departs from the first stop of the trip with the specified headway.
47 | *
48 | * @return DateTime
49 | */
50 | public function getStartTime(): DateTime
51 | {
52 | return DateTime::createFromFormat("H:i:s", $this->start_time);
53 | }
54 |
55 | /**
56 | * Time at which service changes to a different headway (or ceases) at the first stop in the trip.
57 | *
58 | * @return DateTime
59 | */
60 | public function getEndTime(): DateTime
61 | {
62 | return DateTime::createFromFormat("H:i:s", $this->end_time);
63 | }
64 |
65 | /**
66 | * Time, in seconds, between departures from the same stop (headway) for the trip, during the time
67 | * interval specified by start_time and end_time. Multiple headways for the same trip are allowed,
68 | * but may not overlap. New headways may start at the exact time the previous headway ends.
69 | *
70 | * @return int
71 | */
72 | public function getHeadwaySecs(): int
73 | {
74 | return $this->headway_secs;
75 | }
76 |
77 | /**
78 | * Indicates the type of service for a trip. See the file description for more information. Valid options are:
79 | *
80 | * 0 or empty - Frequency-based trips.
81 | * 1 - Schedule-based trips with the exact same headway throughout the day. In this case the end_time value
82 | * must be greater than the last desired trip start_time but less than the last desired trip
83 | * start_time + headway_secs.
84 | *
85 | * @return int|null
86 | */
87 | public function getExactTimes(): ?int
88 | {
89 | return $this->exact_times;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/Transfer.php:
--------------------------------------------------------------------------------
1 | $value) {
29 | $this->$variable = isset($value) && $value !== '' ? $value : null;
30 | }
31 | $this->archive = $archive;
32 | }
33 |
34 | /**
35 | * Identifies a stop or station where a connection between routes begins. If this field refers to a station, the
36 | * transfer rule applies to all of its child stops.
37 | *
38 | * @return string
39 | */
40 | public function getFromStopId(): string
41 | {
42 | return $this->from_stop_id;
43 | }
44 |
45 | /**
46 | * Identifies a stop or station where a connection between routes ends. If this field refers to a station, the
47 | * transfer rule applies to all of its child stops.
48 | *
49 | * @return string
50 | */
51 | public function getToStopId(): string
52 | {
53 | return $this->to_stop_id;
54 | }
55 |
56 | /**
57 | * Indicates the type of connection for the specified pair (from_stop_id, to_stop_id). The following are valid
58 | * values for this field:
59 | *
60 | * 0 or (empty): This is a recommended transfer point between routes.
61 | * 1: This is a timed transfer point between two routes. The departing vehicle is expected to wait for the arriving
62 | * one, with sufficient time for a rider to transfer between routes.
63 | * 2: This transfer requires a minimum amount of time between arrival and departure to ensure a connection. The
64 | * time required to transfer is specified by min_transfer_time.
65 | * 3: Transfers aren't possible between routes at this location.
66 | *
67 | * @return int
68 | */
69 | public function getTransferType(): int
70 | {
71 | return $this->transfer_type;
72 | }
73 |
74 | /**
75 | * Defines the amount of time, in seconds, that must be available in an itinerary to permit a transfer between
76 | * routes at the specified stops. The min_transfer_time must be sufficient to permit a typical rider to move
77 | * between the two stops, as well as some buffer time to allow for schedule variance on each route.
78 | *
79 | * @return int|null
80 | */
81 | public function getMinTransferTime(): ?int
82 | {
83 | return $this->min_transfer_time;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/ShapePoint.php:
--------------------------------------------------------------------------------
1 | $value) {
30 | $this->$variable = isset($value) && $value !== '' ? $value : null;
31 | }
32 | $this->archive = $archive;
33 | }
34 |
35 | /**
36 | * Identifies a shape.
37 | *
38 | * @return string
39 | */
40 | public function getShapeId(): string
41 | {
42 | return $this->shape_id;
43 | }
44 |
45 | /**
46 | * Associates a shape point's latitude with a shape ID. Each row in shapes.txt represents a shape point used to
47 | * define the shape.
48 | *
49 | * @return float
50 | */
51 | public function getShapePtLat(): float
52 | {
53 | return $this->shape_pt_lat;
54 | }
55 |
56 | /**
57 | * Associates a shape point's longitude with a shape ID. Each row in shapes.txt represents a shape point used to
58 | * define the shape.
59 | *
60 | * @return float
61 | */
62 | public function getShapePtLon(): float
63 | {
64 | return $this->shape_pt_lon;
65 | }
66 |
67 | /**
68 | * Associates the latitude and longitude of a shape point with its sequence order along the shape. The values for
69 | * shape_pt_sequence must increase throughout the trip but don't need to be consecutive.
70 | *
71 | * @return int
72 | */
73 | public function getShapePtSequence(): int
74 | {
75 | return $this->shape_pt_sequence;
76 | }
77 |
78 | /**
79 | * Provides the actual distance traveled along the shape from the first shape point to the point specified in this
80 | * record. This information allows the trip planner to determine how much of the shape to draw when they show part
81 | * of a trip on the map. The values used for shape_dist_traveled must increase along with shape_pt_sequence: they
82 | * can't be used to show reverse travel along a route.
83 | *
84 | * The units used for shape_dist_traveled in the shapes.txt file must match the units that are used for this field
85 | * in the stop_times.txt file.
86 | *
87 | * @return float | null
88 | */
89 | public function getShapeDistTraveled(): ?float
90 | {
91 | return ((string) $this->shape_dist_traveled) != '' ? $this->shape_dist_traveled : null;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/Agency.php:
--------------------------------------------------------------------------------
1 | $value) {
34 | $this->$variable = isset($value) && $value !== '' ? $value : null;
35 | }
36 | $this->archive = $archive;
37 | }
38 |
39 | /**
40 | * Identifies a transit brand, which is often the same as a transit agency. Note that in some cases, such as when a
41 | * single agency operates multiple separate services, agencies and brands are distinct. This document uses the term
42 | * "agency" in place of "brand."
43 | *
44 | * A transit feed can represent data from more than one agency. This field is required for transit feeds that
45 | * contain data for multiple agencies. Otherwise, it's optional.
46 | *
47 | * @return string
48 | */
49 | public function getAgencyId(): ?string
50 | {
51 | return $this->agency_id;
52 | }
53 |
54 | /**
55 | * Contains the full name of the transit agency.
56 | *
57 | * @return string
58 | */
59 | public function getAgencyName(): string
60 | {
61 | return $this->agency_name;
62 | }
63 |
64 | /**
65 | * Contains the URL of the transit agency.
66 | *
67 | * @return string
68 | */
69 | public function getAgencyUrl(): string
70 | {
71 | return $this->agency_url;
72 | }
73 |
74 | /**
75 | * Contains the timezone where the transit agency is located. If multiple agencies are specified in the feed,
76 | * each must have the same agency_timezone.
77 | *
78 | * @return null|string
79 | */
80 | public function getAgencyTimezone(): ?string
81 | {
82 | return $this->agency_timezone;
83 | }
84 |
85 | /**
86 | * Specifies the primary language used by this transit agency. This setting helps GTFS consumers choose
87 | * capitalization rules and other language-specific settings for the dataset.
88 | *
89 | * @return null|string
90 | */
91 | public function getAgencyLang(): ?string
92 | {
93 | return $this->agency_lang;
94 | }
95 |
96 | /**
97 | * Provides a voice telephone number for the specified agency. This field is a string value that presents the
98 | * telephone number in a format typical for the agency's service area. It can and should contain punctuation
99 | * marks to group the digits of the number. Dialable text, such as TriMet's 503-238-RIDE, is permitted, but the
100 | * field must not contain any other descriptive text.
101 | *
102 | * @return null|string
103 | */
104 | public function getAgencyPhone(): ?string
105 | {
106 | return $this->agency_phone;
107 | }
108 |
109 | /**
110 | * Specifies the URL of a web page where a rider can purchase tickets or other fare instruments for the agency
111 | * online.
112 | *
113 | * @return null|string
114 | */
115 | public function getAgencyFareUrl(): ?string
116 | {
117 | return $this->agency_fare_url;
118 | }
119 |
120 | /**
121 | * Contains a valid email address that's actively monitored by the agency's customer service department. This email
122 | * address needs to be a direct contact point where transit riders can reach a customer service representative at
123 | * the agency.
124 | *
125 | * @return null|string
126 | */
127 | public function getAgencyEmail(): ?string
128 | {
129 | return $this->agency_email;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Util/Internal/GtfsParserUtil.php:
--------------------------------------------------------------------------------
1 | $value) {
43 | $rowData[$fieldNames[$k]] = $value;
44 | }
45 | if ($indexField == null) {
46 | $resultingObjects[] = new $dataModelClass($gtfsArchive, $rowData);
47 | } else {
48 | $resultingObjects[$rowData[$indexField]] = new $dataModelClass($gtfsArchive, $rowData);
49 | }
50 | }
51 | if (!feof($handle)) {
52 | throw new RuntimeException("Failed to read data from file");
53 | }
54 | fclose($handle);
55 | }
56 | return $resultingObjects;
57 | }
58 |
59 | /**
60 | * This is a modified version of deserializeCSV, in order to optimize the speed when handling shapes
61 | *
62 | * @param $csvPath string File path leading to the CSV file.
63 | *
64 | * @return array the deserialized data, sorted by shape_id.
65 | */
66 | public static function deserializeCSVWithCompositeIndex(GtfsArchive $gtfsArchive, string $csvPath,
67 | string $dataModelClass, $firstIndexField, $secondIndexField): array
68 | {
69 | // Open the CSV file and read it into an associative array
70 | $resultingObjects = $fieldNames = [];
71 |
72 | $handle = self::openFile($csvPath);
73 | if ($handle) {
74 | while (($row = fgetcsv($handle)) !== false) {
75 | $row = array_map('trim', $row);
76 | // Read the header row
77 | if (empty($fieldNames)) {
78 | $fieldNames = $row;
79 | continue;
80 | }
81 | // Read a data row
82 | $rowData = [];
83 | foreach ($row as $k => $value) {
84 | $rowData[$fieldNames[$k]] = $value;
85 | }
86 | $index = $rowData[$firstIndexField] . '-' . $rowData[$secondIndexField];
87 | $resultingObjects[$index] = new $dataModelClass($gtfsArchive, $rowData);
88 | }
89 | if (!feof($handle)) {
90 | throw new RuntimeException("Failed to read data from file");
91 | }
92 | fclose($handle);
93 | }
94 | return $resultingObjects;
95 | }
96 |
97 | /**
98 | * Opens a file and removes BOM if present
99 | *
100 | * @param string $path
101 | *
102 | * @return false|resource resource for the opened file or false
103 | */
104 | private static function openFile(string $path)
105 | {
106 | $handle = @fopen($path, "r");
107 | // exclude BOM
108 | if ($handle && (fgets($handle, 4) !== "\xef\xbb\xbf")) {
109 | rewind($handle);
110 | }
111 | return $handle;
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/FeedInfo.php:
--------------------------------------------------------------------------------
1 | $value) {
34 | $this->$variable = isset($value) && $value !== '' ? $value : null;
35 | }
36 | $this->archive = $archive;
37 | }
38 |
39 | /**
40 | * Contains the full name of the organization that publishes the dataset. This can be the same as one of the
41 | * agency_name values in agency.txt.
42 | *
43 | * @return mixed
44 | */
45 | public function getFeedPublisherName(): string
46 | {
47 | return $this->feed_publisher_name;
48 | }
49 |
50 | /**
51 | * Contains the URL of the dataset publishing organization's website. This can be the same as one of the agency_url
52 | * values in agency.txt.
53 | *
54 | * @return mixed
55 | */
56 | public function getFeedPublisherUrl(): string
57 | {
58 | return $this->feed_publisher_url;
59 | }
60 |
61 | /**
62 | * Specifies the default language used for the text in this dataset. This setting helps GTFS consumers choose
63 | * capitalization rules and other language-specific settings for the feed.
64 | *
65 | * @return mixed
66 | */
67 | public function getFeedLang(): string
68 | {
69 | return $this->feed_lang;
70 | }
71 |
72 | /**
73 | * The dataset provides complete and reliable schedule information for service in the period from the beginning of
74 | * the feed_start_date day to the end of the feed_end_date day.
75 | *
76 | * If both feed_end_date and feed_start_date are given, the end date must not precede the start date. Dataset
77 | * providers are encouraged to give schedule data outside this period to advise passengers of likely future
78 | * service, but dataset consumers should be mindful of its non-authoritative status.
79 | *
80 | * If calendar.txt and calendar_dates.txt omit any active calendar dates that are included within the timeframe
81 | * defined by feed_start_date and feed_end_date, this is an explicit statement that there's no service on those
82 | * omitted days. That is, calendar.txt and calendar_dates.txt are assumed to be an exhaustive list of the dates
83 | * when service is provided.
84 | *
85 | * @return DateTime | null
86 | */
87 | public function getFeedStartDate(): ?DateTime
88 | {
89 | if ($this->feed_start_date == null) return null;
90 | return DateTime::createFromFormat("Ymd", $this->feed_start_date);
91 | }
92 |
93 | /**
94 | * For details on this field, see feed_start_date.
95 | *
96 | * @return DateTime | null
97 | */
98 | public function getFeedEndDate(): ?DateTime
99 | {
100 | if ($this->feed_end_date == null) return null;
101 | return DateTime::createFromFormat("Ymd", $this->feed_end_date);
102 | }
103 |
104 | /**
105 | * Specifies a string that indicates the current version of the GTFS dataset. GTFS-consuming applications can
106 | * display this value to help dataset publishers determine whether the latest version of their dataset has been
107 | * incorporated.
108 | *
109 | * @return null|string
110 | */
111 | public function getFeedVersion(): ?string
112 | {
113 | return $this->feed_version;
114 | }
115 |
116 | /**
117 | * Provides an email address for communication about the GTFS dataset and data publishing practices. The
118 | * feed_contact_email field provides a technical contact for GTFS-consuming applications. To provide customer
119 | * service contact information, use the fields in agency.txt.
120 | *
121 | * @return null|string
122 | */
123 | public function getFeedContactEmail(): ?string
124 | {
125 | return $this->feed_contact_email;
126 | }
127 |
128 | /**
129 | * Provides a URL for contact information, a web form, support desk, or other tool for communication about the GTFS
130 | * dataset and data publishing practices. The feed_contact_url field provides a technical contact for
131 | * GTFS-consuming applications. To provide customer service contact information, use the fields in agency.txt.
132 | *
133 | * @return null|string
134 | */
135 | public function getFeedContactUrl(): ?string
136 | {
137 | return $this->feed_contact_url;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Trafiklab GTFS(-RT) PHP SDK
2 |
3 | [](https://travis-ci.com/trafiklab/gtfs-php-sdk)
4 | [](https://packagist.org/packages/trafiklab/gtfs-php-sdk)
5 | [](https://codecov.io/gh/trafiklab/gtfs-php-sdk)
6 | [](https://opensource.org/licenses/MPL-2.0)
7 |
8 | This SDK makes it easier for developers to use GTFS data in their PHP projects. At this moment, only static files are supported.
9 |
10 | ## Installation
11 | You can install this package through composer
12 |
13 | ```composer require trafiklab/gtfs-php-sdk```
14 |
15 | ## Usage and examples
16 |
17 | **Opening a GTFS file**
18 |
19 | You can either load a local GTFS zip file, or you can download it over HTTP
20 |
21 | ```php
22 | $gtfsArchive = GtfsArchive::createFromPath("gtfs.zip");
23 | $gtfsArchive = GtfsArchive::createFromUrl("http://example.com/gtfs.zip");
24 | ```
25 |
26 | Optionally, you can choose to download a GTFS zip file only if it has changed since the last retrieval. This is useful when trying to automate GTFS retrievals that need to be stored within a database, without constantly rewriting the same data each time.
27 | The following Http Headers are used:
28 |
29 | - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
30 | - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
31 | - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
32 |
33 | Most agencies provide a Last-Modified header, but if it's not present, ETag is the best route to go. If for some reason ETag is also not provided, it will just continue normally as if you were to use the original GtfsArchive::createFromUrl() method.
34 |
35 | ```php
36 | $gtfsArchive = GtfsArchive::createFromUrlIfModified(
37 | "http://example.com/gtfs.zip",
38 | "Wed, 10 Jun 2020 15:56:14 GMT",
39 | "99fa-5a7bce236c526"
40 | );
41 | ```
42 |
43 | If you don't have an ETag or Last-Modified value to begin with, simply leave them out and the method will retrieve them for you.
44 |
45 | ```php
46 | if ($gtfsArchive = GtfsArchive::createFromUrlIfModified("http://example.com/gtfs.zip") {
47 | // Get Methods return null if GTFS Url does not contain the specified Header: ETag, Last-Modified.
48 | $lastModified = $gtfsArchive->getLastModified(); // Wed, 10 Jun 2020 15:56:14 GMT | null
49 | $eTag = $gtfsArchive->getETag(); // "99fa-5a7bce236c526" | null
50 |
51 | // You can get the Last-Modified datetime PHP Object (Useful for storing in databases) by doing the following:
52 | $datetime = $gtfsArchive->getLastModifiedDateTime() // DateTime Object | null
53 |
54 | // Or Convert it back to a String using the standard Last-Modified HTTP header format.
55 | if ($datetime) {
56 | $lastModified = GtfsArchive::getLastModifiedFromDateTime($datetime); // Wed, 10 Jun 2020 15:56:14 GMT
57 | }
58 | }
59 |
60 | ```
61 |
62 |
63 | Files are extracted to a temporary directory (/tmp/gtfs/), and cleaned up when the GtfsArchive object is destructed.
64 | You can call `$gtfsArchive->deleteUncompressedFiles()` to manually remove the uncompressed files.
65 |
66 | **Reading a file**
67 | ```php
68 | $agencyTxt = $gtfsArchive->getAgencyFile(); // read agency.txt
69 | $calendarTxt = $gtfsArchive->getCalendarFile(); // read calendar.txt
70 | $routesTxt = $gtfsArchive->getRoutesFile(); // read routes.txt
71 | $stopTimesTxt = $gtfsArchive->getStopTimesFile(); // read stop_times.txt
72 | ...
73 | ```
74 |
75 | All files are lazy loaded and cached. This means that data is only loaded after calling a method such as `getStopTimesFile()`.
76 | Keep in mind that in can take a while to read the data for the first time. It can take up to a minute to read a large `stop_times.txt` file.
77 |
78 | **Reading file data**
79 |
80 | Every file class contains a method to read all data in that file. Some classes contain additional helper methods for frequently used queries such as filtering by an id or foreign key.
81 |
82 | There is one PHP class for every (supported) file, and another class for the data contained in one row of that file. The definition of each field is contained in the PHPDoc for each getter function, allowing you to focus on coding, and less on alt-tabbing between specification and code.
83 | ```php
84 | $stopTimesTxt = $gtfsArchive->getStopTimesFile(); // The file is represented by a StopTimesFile object
85 | $allStopTimes = $stopTimesTxt->getStopTimes(); // a method like this is always available
86 | $stopTimesForStopX = $stopTimesTxt->getStopTimesForStop($stopId); // this is a helper method for foreign keys
87 |
88 | $stopTime = $allStopTimes[0]; // Get the first row
89 | $headsign = $stopTime->getStopHeadsign(); // One row of data is represented by a StopTime object
90 | ```
91 |
92 | ## Contributing
93 |
94 | We accept pull requests, but please create an issue first in order to discuss the addition or fix.
95 | If you would like to see a new feature added, you can also create a feature request by creating an issue.
96 |
97 | ## Help
98 |
99 | If you're stuck with a question, feel free to ask help through the Issue tracker.
100 | - Need help with API keys? Please read [www.trafiklab.se/api-nycklar](https://www.trafiklab.se/api-nycklar) first.
101 | - Do you want to check the current systems status? Service disruptions
102 | are published on the [Trafiklab homepage](https://www.trafiklab.se/)
103 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/Trip.php:
--------------------------------------------------------------------------------
1 | $value) {
35 | $this->$variable = isset($value) && $value !== '' ? $value : null;
36 | }
37 | $this->archive = $archive;
38 | }
39 |
40 | /**
41 | * Identifies a route.
42 | *
43 | * @return string
44 | */
45 | public function getRouteId(): string
46 | {
47 | return $this->route_id;
48 | }
49 |
50 | /**
51 | * Identifies a set of dates when service is available for one or more routes.
52 | *
53 | * @return string
54 | */
55 | public function getServiceId(): string
56 | {
57 | return $this->service_id;
58 | }
59 |
60 | /**
61 | * Identifies a trip.
62 | *
63 | * @return string
64 | */
65 | public function getTripId(): string
66 | {
67 | return $this->trip_id;
68 | }
69 |
70 | /**
71 | * Contains the text that appears on signage that identifies the trip's destination to riders. Use this field to
72 | * distinguish between different patterns of service on the same route.
73 | *
74 | * If the headsign changes during a trip, you can override the trip_headsign with values from the stop_headsign
75 | * field in stop_times.txt.
76 | *
77 | * @return string | null
78 | */
79 | public function getTripHeadsign(): ?string
80 | {
81 | return $this->trip_headsign;
82 | }
83 |
84 | /**
85 | * Contains the public-facing text that's shown to riders to identify the trip, such as the train numbers for
86 | * commuter rail trips. If riders don't commonly rely on trip names, leave this field blank.
87 | *
88 | * If a trip_short_name is provided, it needs to uniquely identify a trip within a service day. Don't use it for
89 | * destination names or limited/express designations.
90 | *
91 | * @return string | null
92 | */
93 | public function getTripShortName(): ?string
94 | {
95 | return $this->trip_short_name;
96 | }
97 |
98 | /**
99 | * Indicates the direction of travel for a trip. Use this field to distinguish between bi-directional trips with
100 | * the same route_id. The following are valid values for this field:
101 | *
102 | * 0: Travel in one direction of your choice, such as outbound travel.
103 | * 1: Travel in the opposite direction, such as inbound travel.
104 | *
105 | * This field isn't used in routing, but instead provides a way to separate trips by direction when you publish
106 | * time tables. You can specify names for each direction with the trip_headsign field.
107 | *
108 | * @return int | null
109 | */
110 | public function getDirectionId(): ?int
111 | {
112 | return $this->direction_id;
113 | }
114 |
115 | /**
116 | * Identifies the block to which the trip belongs. A block consists of a single trip or many sequential trips made
117 | * with the same vehicle. The trips are grouped into a block by the use of a shared service day and block_id. A
118 | * block_id can include trips with different service days, which then makes distinct blocks. For more details, see
119 | * Blocks and service days example.
120 | *
121 | * @return string | null
122 | */
123 | public function getBlockId(): ?string
124 | {
125 | return $this->block_id;
126 | }
127 |
128 | /**
129 | * Defines a geospatial shape that describes the vehicle travel for a trip.
130 | *
131 | * @return string | null
132 | */
133 | public function getShapeId(): ?string
134 | {
135 | return $this->shape_id;
136 | }
137 |
138 | /**
139 | * Identifies whether wheelchair boardings are possible for the specified trip. This field can have the following
140 | * values:
141 | *
142 | * 0 or (empty): There's no accessibility information available for this trip.
143 | * 1: The vehicle used on this particular trip can accommodate at least one rider in a wheelchair.
144 | * 2: No riders in wheelchairs can be accommodated on this trip.
145 | *
146 | * @return int | null
147 | */
148 | public function getWheelchairAccessible(): ?int
149 | {
150 | return $this->wheelchair_accessible;
151 | }
152 |
153 | /**
154 | * Identifies whether bicycles are allowed on the specified trip. This field can have the following values:
155 | *
156 | * 0 or (empty): There's no bike information available for the trip.
157 | * 1: The vehicle used on this particular trip can accommodate at least one bicycle.
158 | * 2: No bicycles are allowed on this trip.
159 | *
160 | * @return int | null
161 | */
162 | public function getBikesAllowed(): ?int
163 | {
164 | return $this->bikes_allowed;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/Route.php:
--------------------------------------------------------------------------------
1 | $value) {
37 | $this->$variable = isset($value) && $value !== '' ? $value : null;
38 | }
39 | $this->archive = $archive;
40 | }
41 |
42 | /**
43 | * Identifies a route.
44 | *
45 | * @return string
46 | */
47 | public function getRouteId(): string
48 | {
49 | return $this->route_id;
50 | }
51 |
52 | /**
53 | * Defines an agency for the specified route. This field is required when the dataset provides data for routes from
54 | * more than one agency in agency.txt. Otherwise, it's optional.
55 | *
56 | * @return string
57 | */
58 | public function getAgencyId(): ?string
59 | {
60 | return $this->agency_id;
61 | }
62 |
63 | /**
64 | * Contains the short name of a route. This is a short, abstract identifier like 32, 100X, or Green that riders use
65 | * to identify a route, but which doesn't give any indication of what places the route serves.
66 | *
67 | * At least one of route_short_name or route_long_name must be specified, or both if appropriate. If the route has
68 | * no short name, specify a route_long_name and use an empty string as the value for this field.
69 | *
70 | * @return string
71 | */
72 | public function getRouteShortName(): ?string
73 | {
74 | return $this->route_short_name;
75 | }
76 |
77 | /**
78 | * Contains the full name of a route. This name is generally more descriptive than the name from route_short_name
79 | * and often includes the route's destination or stop.
80 | *
81 | * At least one of route_short_name or route_long_name must be specified, or both if appropriate. If the route has
82 | * no long name, specify a route_short_name and use an empty string as the value for this field.
83 | *
84 | * @return string
85 | */
86 | public function getRouteLongName(): ?string
87 | {
88 | return $this->route_long_name;
89 | }
90 |
91 | /**
92 | * Describes a route. Be sure to provide useful, quality information for this field. Don't simply duplicate the
93 | * name of the route.
94 | *
95 | * @return string
96 | */
97 | public function getRouteDesc(): ?string
98 | {
99 | return $this->route_desc;
100 | }
101 |
102 | /**
103 | * Describes the type of transportation used on a route. The following are valid values for this field:
104 | *
105 | * 0: Tram, streetcar, or light rail. Used for any light rail or street-level system within a metropolitan area.
106 | * 1: Subway or metro. Used for any underground rail system within a metropolitan area.
107 | * 2: Rail. Used for intercity or long-distance travel.
108 | * 3: Bus. Used for short- and long-distance bus routes.
109 | * 4: Ferry. Used for short- and long-distance boat service.
110 | * 5: Cable car. Used for street-level cable cars where the cable runs beneath the car.
111 | * 6: Gondola or suspended cable car. Typically used for aerial cable cars where the car is suspended from the
112 | * cable.
113 | * 7: Funicular. Used for any rail system that moves on steep inclines with a cable traction system.
114 | *
115 | * @return int
116 | */
117 | public function getRouteType(): int
118 | {
119 | return $this->route_type;
120 | }
121 |
122 | /**
123 | * Contains the URL of a web page for a particular route. This URL needs to be different from the agency_url value.
124 | *
125 | * @return string| null
126 | */
127 | public function getRouteUrl(): ?string
128 | {
129 | return $this->route_url;
130 | }
131 |
132 | /**
133 | *
134 | *
135 | * In systems that assigns colors to routes, the route_color field defines a color that corresponds to a route. If
136 | * no color is specified, the default route color is white, FFFFFF.
137 | *
138 | * The color difference between route_color and route_text_color needs to provide sufficient contrast when viewed
139 | * on a black and white screen. The W3C techniques for accessibility evaluation and repair tools document offers a
140 | * useful algorithm to evaluate color contrast. There are also helpful online tools to help you choose contrasting
141 | * colors, such as the snook.ca Color Contrast Check application.
142 | *
143 | * @return string| null
144 | */
145 | public function getRouteColor(): ?string
146 | {
147 | return $this->route_color;
148 | }
149 |
150 | /**
151 | * Specifies a legible color for text that's drawn against the background color of route_color. If no color is
152 | * specified, the default text color is black, 000000.
153 | *
154 | * The color difference between route_color and route_text_color needs to provide sufficient contrast when viewed
155 | * on a black and white screen. The W3C techniques for accessibility evaluation and repair tools document offers a
156 | * useful algorithm to evaluate color contrast. There are also helpful online tools to help you choose contrasting
157 | * colors, such as the snook.ca Color Contrast Check application.
158 | *
159 | * @return string| null
160 | */
161 | public function getRouteTextColor(): ?string
162 | {
163 | return $this->route_text_color;
164 | }
165 |
166 | /**
167 | * Specifies the order in which to present the routes to customers. Routes with smaller route_sort_order values
168 | * need to be displayed before routes with larger route_sort_order values.
169 | *
170 | * @return int | null
171 | */
172 | public function getRouteSortOrder(): ?int
173 | {
174 | return $this->route_sort_order;
175 | }
176 |
177 | /**
178 | * Indicates whether a rider can board the transit vehicle anywhere along the vehicle’s travel path. The path is described by shapes.txt on every trip of the route. Valid options are:
179 | *
180 | * 0 - Continuous stopping pickup.
181 | * 1 or empty - No continuous stopping pickup.
182 | * 2 - Must phone an agency to arrange continuous stopping pickup.
183 | * 3 - Must coordinate with a driver to arrange continuous stopping pickup.
184 | *
185 | * The default continuous pickup behavior defined in routes.txt can be overridden in stop_times.txt.
186 | *
187 | * @return int | null
188 | */
189 | public function getContinuousPickup(): ?int
190 | {
191 | return $this->continuous_pickup;
192 | }
193 |
194 | /**
195 | * Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path. The path is described by shapes.txt on every trip of the route. Valid options are:
196 | *
197 | * 0- Continuous stopping drop-off.
198 | * 1 or empty - No continuous stopping drop-off.
199 | * 2 - Must phone an agency to arrange continuous stopping drop-off.
200 | * 3 - Must coordinate with a driver to arrange continuous stopping drop-off.
201 | *
202 | * The default continuous drop-off behavior defined in routes.txt can be overridden in stop_times.txt.
203 | *
204 | * @return int | null
205 | */
206 | public function getContinuousDropOff(): ?int
207 | {
208 | return $this->continuous_drop_off;
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/GtfsRouteType.php:
--------------------------------------------------------------------------------
1 | 'Tram, streetcar, or light rail',
11 | '1' => 'Subway or metro',
12 | '2' => 'Rail',
13 | '3' => 'Bus',
14 | '4' => 'Ferry',
15 | '5' => 'Cable car',
16 | '6' => 'Gondola or suspended cable car',
17 | '7' => 'Funicular',
18 | '100' => 'Railway Service',
19 | '101' => 'High Speed Rail Service',
20 | '102' => 'Long Distance Trains',
21 | '103' => 'Inter Regional Rail Service',
22 | '104' => 'Car Transport Rail Service',
23 | '105' => 'Sleeper Rail Service',
24 | '106' => 'Regional Rail Service',
25 | '107' => 'Tourist Railway Service',
26 | '108' => 'Rail Shuttle (Within Complex)',
27 | '109' => 'Suburban Railway',
28 | '110' => 'Replacement Rail Service',
29 | '111' => 'Special Rail Service',
30 | '112' => 'Lorry Transport Rail Service',
31 | '113' => 'All Rail Services',
32 | '114' => 'Cross-Country Rail Service',
33 | '115' => 'Vehicle Transport Rail Service',
34 | '116' => 'Rack and Pinion Railway',
35 | '117' => 'Additional Rail Service',
36 | '200' => 'Coach Service',
37 | '201' => 'International Coach Service',
38 | '202' => 'National Coach Service',
39 | '203' => 'Shuttle Coach Service',
40 | '204' => 'Regional Coach Service',
41 | '205' => 'Special Coach Service',
42 | '206' => 'Sightseeing Coach Service',
43 | '207' => 'Tourist Coach Service',
44 | '208' => 'Commuter Coach Service',
45 | '209' => 'All Coach Services',
46 | '300' => 'Suburban Railway Service',
47 | '400' => 'Urban Railway Service',
48 | '401' => 'Metro Service',
49 | '402' => 'Underground Service',
50 | '403' => 'Urban Railway Service',
51 | '404' => 'All Urban Railway Services',
52 | '405' => 'Monorail',
53 | '500' => 'Metro Service',
54 | '600' => 'Underground Service',
55 | '700' => 'Bus Service',
56 | '701' => 'Regional Bus Service',
57 | '702' => 'Express Bus Service',
58 | '703' => 'Stopping Bus Service',
59 | '704' => 'Local Bus Service',
60 | '705' => 'Night Bus Service',
61 | '706' => 'Post Bus Service',
62 | '707' => 'Special Needs Bus',
63 | '708' => 'Mobility Bus Service',
64 | '709' => 'Mobility Bus for Registered Disabled',
65 | '710' => 'Sightseeing Bus',
66 | '711' => 'Shuttle Bus',
67 | '712' => 'School Bus',
68 | '713' => 'School and Public Service Bus',
69 | '714' => 'Rail Replacement Bus Service',
70 | '715' => 'Demand and Response Bus Service',
71 | '716' => 'All Bus Services',
72 | '800' => 'Trolleybus Service',
73 | '900' => 'Tram Service',
74 | '901' => 'City Tram Service',
75 | '902' => 'Local Tram Service',
76 | '903' => 'Regional Tram Service',
77 | '904' => 'Sightseeing Tram Service',
78 | '905' => 'Shuttle Tram Service',
79 | '906' => 'All Tram Services',
80 | '1000' => 'Water Transport Service',
81 | '1001' => 'International Car Ferry Service',
82 | '1002' => 'National Car Ferry Service',
83 | '1003' => 'Regional Car Ferry Service',
84 | '1004' => 'Local Car Ferry Service',
85 | '1005' => 'International Passenger Ferry Service',
86 | '1006' => 'National Passenger Ferry Service',
87 | '1007' => 'Regional Passenger Ferry Service',
88 | '1008' => 'Local Passenger Ferry Service',
89 | '1009' => 'Post Boat Service',
90 | '1010' => 'Train Ferry Service',
91 | '1011' => 'Road-Link Ferry Service',
92 | '1012' => 'Airport-Link Ferry Service',
93 | '1013' => 'Car High-Speed Ferry Service',
94 | '1014' => 'Passenger High-Speed Ferry Service',
95 | '1015' => 'Sightseeing Boat Service',
96 | '1016' => 'School Boat',
97 | '1017' => 'Cable-Drawn Boat Service',
98 | '1018' => 'River Bus Service',
99 | '1019' => 'Scheduled Ferry Service',
100 | '1020' => 'Shuttle Ferry Service',
101 | '1021' => 'All Water Transport Services',
102 | '1100' => 'Air Service',
103 | '1101' => 'International Air Service',
104 | '1102' => 'Domestic Air Service',
105 | '1103' => 'Intercontinental Air Service',
106 | '1104' => 'Domestic Scheduled Air Service',
107 | '1105' => 'Shuttle Air Service',
108 | '1106' => 'Intercontinental Charter Air Service',
109 | '1107' => 'International Charter Air Service',
110 | '1108' => 'Round-Trip Charter Air Service',
111 | '1109' => 'Sightseeing Air Service',
112 | '1110' => 'Helicopter Air Service',
113 | '1111' => 'Domestic Charter Air Service',
114 | '1112' => 'Schengen-Area Air Service',
115 | '1113' => 'Airship Service',
116 | '1114' => 'All Air Services',
117 | '1200' => 'Ferry Service',
118 | '1300' => 'Telecabin Service',
119 | '1301' => 'Telecabin Service',
120 | '1302' => 'Cable Car Service',
121 | '1303' => 'Elevator Service',
122 | '1304' => 'Chair Lift Service',
123 | '1305' => 'Drag Lift Service',
124 | '1306' => 'Small Telecabin Service',
125 | '1307' => 'All Telecabin Services',
126 | '1400' => 'Funicular Service',
127 | '1401' => 'Funicular Service',
128 | '1402' => 'All Funicular Service',
129 | '1500' => 'Taxi Service',
130 | '1501' => 'Communal Taxi Service',
131 | '1502' => 'Water Taxi Service',
132 | '1503' => 'Rail Taxi Service',
133 | '1504' => 'Bike Taxi Service',
134 | '1505' => 'Licensed Taxi Service',
135 | '1506' => 'Private Hire Service Vehicle',
136 | '1507' => 'All Taxi Services',
137 | '1600' => 'Self Drive',
138 | '1601' => 'Hire Car',
139 | '1602' => 'Hire Van',
140 | '1603' => 'Hire Motorbike',
141 | '1604' => 'Hire Cycle',
142 | '1700' => 'Miscellaneous Service',
143 | '1701' => 'Cable Car',
144 | '1702' => 'Horse-drawn Carriage',
145 | ];
146 |
147 | const TRAM_TYPES = [
148 | '0', '900', '901', '902', '903', '904', '905', '906',
149 | ];
150 |
151 | CONST METRO_TYPES = [
152 | '1', '401', '402', '500', '600',
153 | ];
154 | CONST TRAIN_TYPES = [
155 | '2', '100', '101', '102', '103', '104', '105', '106', '107', '108', '109', '110', '111', '112', '113', '114',
156 | '115', '116', '117',
157 | ];
158 | const BUS_TYPES = [
159 | '3', '200', '201', '202', '203', '204', '205', '206', '207', '208', '209', '700', '701', '702', '703', '704',
160 | '705', '706', '707', '708', '709', '710', '711', '712', '713', '714', '715', '716', '800',
161 | ];
162 |
163 | const FERRY_TYPES = [
164 | '4', '1000', '1001', '1002', '1003', '1004', '1005', '1006', '1007', '1008', '1009', '1010', '1011', '1012',
165 | '1013', '1014', '1015', '1016', '1017', '1018', '1019', '1020', '1021',
166 | ];
167 |
168 | public static function isBus(int $type): bool
169 | {
170 | return in_array($type, self::BUS_TYPES);
171 | }
172 |
173 | public static function isMetro(int $type): bool
174 | {
175 | return in_array($type, self::METRO_TYPES);
176 | }
177 |
178 | public static function isTrain(int $type): bool
179 | {
180 | return in_array($type, self::TRAIN_TYPES);
181 | }
182 |
183 | public static function isTram(int $type): bool
184 | {
185 | return in_array($type, self::TRAM_TYPES);
186 | }
187 |
188 | public static function isFerry(int $type): bool
189 | {
190 | return in_array($type, self::FERRY_TYPES);
191 | }
192 |
193 | /**
194 | * Map an extended GTFS route type to a basic route type. This can be useful to group for example bus services.
195 | * This works for tram, metro, train, bus and ferry services.
196 | *
197 | * @param int $type The type to convert.
198 | *
199 | * @return int The converted code. -1 if conversion failed.
200 | */
201 | public static function mapExtendedRouteTypeToBasicRouteType(int $type): int
202 | {
203 |
204 | if (self::isTram($type)) {
205 | return 0;
206 | }
207 |
208 | if (self::isMetro($type)) {
209 | return 1;
210 | }
211 |
212 | if (self::isTrain($type)) {
213 | return 2;
214 | }
215 |
216 | if (self::isBus($type)) {
217 | return 3;
218 | }
219 | if (self::isFerry($type)) {
220 | return 4;
221 | }
222 | return -1;
223 | }
224 | }
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/StopTime.php:
--------------------------------------------------------------------------------
1 | $value) {
37 | $this->$variable = isset($value) && $value !== '' ? $value : null;
38 | }
39 | $this->archive = $archive;
40 | }
41 |
42 | /**
43 | * Identifies a trip.
44 | *
45 | * @return string
46 | */
47 | public function getTripId(): string
48 | {
49 | return $this->trip_id;
50 | }
51 |
52 | /**
53 | * Specifies the arrival time at a specific stop for a specific trip on a route. An arrival time must be specified
54 | * for the first and the last stop in a trip.
55 | *
56 | * If you don't have separate times for arrival and departure at a stop, enter the same value for arrival_time and
57 | * departure_time.
58 | *
59 | * For information on how to enter arrival times for stops where the vehicle strictly adheres to a schedule, see
60 | * Timepoints.
61 | *
62 | * @return string|null
63 | */
64 | public function getArrivalTime(): ?string
65 | {
66 | return $this->arrival_time;
67 | }
68 |
69 | /**
70 | * Specifies the departure time from a specific stop for a specific trip on a route. A departure time must be
71 | * specified for the first and the last stop in a trip, even if the vehicle does not allow boarding at the last
72 | * stop.
73 | *
74 | * If you don't have separate times for arrival and departure at a stop, enter the same value for arrival_time and
75 | * departure_time.
76 | *
77 | * For information on how to enter departure times for stops where the vehicle strictly adheres to a schedule, see
78 | * Timepoints.
79 | *
80 | * @return string|null
81 | */
82 | public function getDepartureTime(): ?string
83 | {
84 | return $this->departure_time;
85 | }
86 |
87 | /**
88 | * Identifies the serviced stop. Multiple routes can use the same stop.
89 | *
90 | * If location_type is used in stops.txt, all stops referenced in stop_times.txt must have location_type=0. Where
91 | * possible, stop_id values should remain consistent between feed updates. In other words, stop A with stop_id=1
92 | * should have stop_id=1 in all subsequent data updates. If a stop isn't a timepoint, enter blank values for
93 | * arrival_time and departure_time. For more details, see Timepoints.
94 | *
95 | * @return string
96 | */
97 | public function getStopId(): string
98 | {
99 | return $this->stop_id;
100 | }
101 |
102 | /**
103 | * Identifies the order of the stops for a particular trip. The values for stop_sequence must increase throughout
104 | * the trip but do not need to be consecutive.
105 | *
106 | * For example, the first stop on the trip could have a stop_sequence of 1, the second stop on the trip could have
107 | * a stop_sequence of 23, the third stop could have a stop_sequence of 40, and so on.
108 | *
109 | * @return int|null
110 | */
111 | public function getStopSequence(): ?int
112 | {
113 | return $this->stop_sequence;
114 | }
115 |
116 | /**
117 | * Contains the text, as shown on signage, that identifies the trip's destination to riders. Use this field to
118 | * override the default trip_headsign when the headsign changes between stops. If this headsign is associated with
119 | * an entire trip, use trip_headsign instead.
120 | *
121 | * @return string|null
122 | */
123 | public function getStopHeadsign(): ?string
124 | {
125 | return $this->stop_headsign;
126 | }
127 |
128 | /**
129 | * Indicates whether riders are picked up at a stop as part of the normal schedule or whether a pickup at the stop
130 | * isn't available. This field also allows the transit agency to indicate that riders must call the agency or
131 | * notify the driver to arrange a pickup at a particular stop. The following are valid values for this field:
132 | *
133 | * 0 or (empty): Regularly scheduled pickup
134 | * 1: No pickup available
135 | * 2: Must phone agency to arrange pickup
136 | * 3: Must coordinate with driver to arrange pickup
137 | *
138 | * @return int|null
139 | */
140 | public function getPickupType(): ?int
141 | {
142 | return $this->pickup_type;
143 | }
144 |
145 | /**
146 | * Indicates whether riders are dropped off at a stop as part of the normal schedule or whether a dropoff at the
147 | * stop is unavailable. This field also allows the transit agency to indicate that riders must call the agency or
148 | * notify the driver to arrange a dropoff at a particular stop. The following are valid values for this field:
149 | *
150 | * 0 or (empty): Regularly scheduled drop off
151 | * 1: No dropoff available
152 | * 2: Must phone agency to arrange dropoff
153 | * 3: Must coordinate with driver to arrange dropoff
154 | *
155 | * @return int|null
156 | */
157 | public function getDropOffType(): ?int
158 | {
159 | return $this->drop_off_type;
160 | }
161 |
162 | /**
163 | * When used in the stop_times.txt file, the shape_dist_traveled field positions a stop as a distance from the
164 | * first shape point. The shape_dist_traveled field represents a real distance traveled along the route in units
165 | * such as feet or kilometers.
166 | *
167 | * For example, if a bus travels a distance of 5.25 kilometers from the start of the shape to the stop, the
168 | * shape_dist_traveled for the stop ID would be entered as 5.25. This information allows the trip planner to
169 | * determine how much of the shape to draw when they show part of a trip on the map.
170 | *
171 | * The values used for shape_dist_traveled must increase along with stop_sequence: they can't be used to show
172 | * reverse travel along a route. The units used for shape_dist_traveled in the stop_times.txt file must match the
173 | * units that are used for this field in the shapes.txt file.
174 | *
175 | * @return float|null
176 | */
177 | public function getShapeDistTraveled(): ?float
178 | {
179 | return ((string) $this->shape_dist_traveled) != '' ? $this->shape_dist_traveled : null;
180 | }
181 |
182 | /**
183 | * Indicates if the specified arrival and departure times for a stop are strictly adhered to by the transit
184 | * vehicle, or if they're instead approximate or interpolated times. This field allows a GTFS producer to provide
185 | * interpolated stop times that potentially incorporate local knowledge, but still indicate if the times are
186 | * approximate.
187 | *
188 | * For stop-time entries with specified arrival and departure times, the following are valid values for this field:
189 | *
190 | * 0: Times are considered approximate.
191 | * 1 or (empty): Times are considered exact.
192 | *
193 | * For stop-time entries without specified arrival and departure times, feed consumers must interpolate arrival and
194 | * departure times. Feed producers can optionally indicate that such an entry is not a timepoint (with
195 | * timepoint=0), but it's an error to mark an entry as a timepoint (with timepoint=1) without specifying arrival
196 | * and departure times.
197 | *
198 | * @return int|null
199 | */
200 | public function getTimepoint(): ?int
201 | {
202 | return $this->timepoint;
203 | }
204 |
205 | /**
206 | * Indicates whether a rider can board the transit vehicle at any point along the vehicle’s travel path. The path is described by shapes.txt, from this stop_time to the next stop_time in the trip’s stop_sequence. Valid options are:
207 | *
208 | * 0 - Continuous stopping pickup.
209 | * 1 or empty - No continuous stopping pickup.
210 | * 2 - Must phone an agency to arrange continuous pickup.
211 | * 3 - Must coordinate with a driver to arrange continuous stopping pickup.
212 | *
213 | * The continuous pickup behavior indicated in stop_times.txt overrides any behavior defined in routes.txt.
214 | *
215 | * @return int|null
216 | */
217 | public function getContinuousPickup(): ?int
218 | {
219 | return $this->continuous_pickup;
220 | }
221 |
222 | /**
223 | * Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path as described by shapes.txt, from this stop_time to the next stop_time in the trip’s stop_sequence.
224 | *
225 | * 0 - Continuous stopping drop off.
226 | * 1 or empty - No continuous stopping drop off.
227 | * 2 - Must phone an agency to arrange continuous drop off.
228 | * 3 - Must coordinate with a driver to arrange continuous stopping drop off.
229 | *
230 | * The continuous drop-off behavior indicated in stop_times.txt overrides any behavior defined in routes.txt.
231 | * @return int|null
232 | */
233 | public function getContinuousDropOff(): ?int
234 | {
235 | return $this->continuous_drop_off;
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/tests/Trafiklab/Gtfs/GtfsArchiveIntegrationTest.php:
--------------------------------------------------------------------------------
1 | gtfsArchive = GtfsArchive::createFromPath("./tests/Resources/Gtfs/minified-test-feed.zip");
21 | }
22 |
23 | function __destruct()
24 | {
25 | $this->gtfsArchive->deleteUncompressedFiles();
26 | }
27 |
28 | public function testGetStops()
29 | {
30 | self::assertEquals(9375, count($this->gtfsArchive->getStopsFile()->getStops()));
31 | }
32 |
33 | public function testGetStop()
34 | {
35 | $stop = $this->gtfsArchive->getStopsFile()->getStop('9021008004033000');
36 |
37 | self::assertEquals('Ottenby gård nedre', $stop->getStopName());
38 | self::assertEquals(56.232823, $stop->getStopLat());
39 | self::assertEquals(16.417789, $stop->getStopLon());
40 | self::assertEquals(1, $stop->getLocationType());
41 | }
42 |
43 | public function testGetRoutes()
44 | {
45 | self::assertEquals(2, count($this->gtfsArchive->getRoutesFile()->getRoutes()));
46 | }
47 |
48 | public function testGetRoute()
49 | {
50 | $route = $this->gtfsArchive->getRoutesFile()->getRoute('9011008842200000');
51 |
52 | self::assertEquals('9011008842200000', $route->getRouteId());
53 | self::assertEquals('88100000000001375', $route->getAgencyId());
54 | self::assertEquals('8422', $route->getRouteShortName());
55 | self::assertEquals('Närtrafik 2', $route->getRouteLongName());
56 | self::assertEquals('0', $route->getRouteType());
57 | }
58 |
59 | public function testGetTrips()
60 | {
61 | self::assertEquals(2, count($this->gtfsArchive->getTripsFile()->getTrips()));
62 | }
63 |
64 | public function testGetTrip()
65 | {
66 | $trip = $this->gtfsArchive->getTripsFile()->getTrip('88100000084251548');
67 |
68 | self::assertEquals('9011008842200000', $trip->getRouteId());
69 | self::assertEquals('2', $trip->getServiceId());
70 | self::assertEquals('88100000084251548', $trip->getTripId());
71 | self::assertEquals('', $trip->getTripHeadsign());
72 | self::assertEquals('0', $trip->getDirectionId());
73 | self::assertEquals('2', $trip->getShapeId());
74 | }
75 |
76 | public function testGetStopTimes()
77 | {
78 | self::assertEquals(92, count($this->gtfsArchive->getStopTimesFile()->getStopTimes()));
79 | }
80 |
81 | public function testGetShapePoints()
82 | {
83 | self::assertEquals(2474, count($this->gtfsArchive->getShapesFile()->getShapePoints()));
84 | }
85 |
86 | public function testGetShape()
87 | {
88 | $shape = $this->gtfsArchive->getShapesFile()->getShape(1);
89 |
90 | self::assertEquals(1236, count($shape));
91 | self::assertEquals(1, $shape[0]->getShapeId());
92 | self::assertEquals(0, $shape[0]->getShapeDistTraveled());
93 |
94 | self::assertEquals(1, $shape[10]->getShapeId());
95 | self::assertEquals(292.63, $shape[10]->getShapeDistTraveled());
96 |
97 | self::assertEquals(56.666811, $shape[0]->getShapePtLat());
98 | self::assertEquals(16.318689, $shape[0]->getShapePtLon());
99 | for ($i = 0; $i < count($shape); $i++) {
100 | self::assertEquals(1, $shape[$i]->getShapeId());
101 | self::assertEquals($i + 1, $shape[$i]->getShapePtSequence());
102 | }
103 |
104 | $shape = $this->gtfsArchive->getShapesFile()->getShape(2185);
105 | self::assertEquals(0, count($shape));
106 | }
107 |
108 | public function testGetShape_shapeIdPresentAtStart()
109 | {
110 | $shape = $this->gtfsArchive->getShapesFile()->getShape(1);
111 | self::assertEquals(1236, count($shape));
112 |
113 | // Additional verification of inner working, this should be in a separate test
114 | $shapeStartId = BinarySearchUtil::findIndexOfFirstFieldOccurrence(
115 | $this->gtfsArchive->getShapesFile()->getShapePoints(), 'getShapeId', 1);
116 | self::assertEquals(0, $shapeStartId);
117 |
118 | $shapeStartId = BinarySearchUtil::findIndexOfFirstFieldOccurrence(
119 | $this->gtfsArchive->getShapesFile()->getShapePoints(), 'getShapeId', 2);
120 | self::assertEquals(1236, $shapeStartId);
121 | }
122 |
123 | public function testGetShape_shapeIdPresentInMiddle()
124 | {
125 | $shape = $this->gtfsArchive->getShapesFile()->getShape(4);
126 | self::assertEquals(1, count($shape));
127 |
128 | // Additional verification of inner working, this should be in a separate test
129 | $shapeStartId = BinarySearchUtil::findIndexOfFirstFieldOccurrence(
130 | $this->gtfsArchive->getShapesFile()->getShapePoints(), 'getShapeId', 4);
131 | self::assertEquals(2472, $shapeStartId);
132 | }
133 |
134 | public function testGetShape_shapeIdPresentAtEnd()
135 | {
136 | // Additional verification of inner working, this should be in a separate test
137 | $shapeStartId = BinarySearchUtil::findIndexOfFirstFieldOccurrence(
138 | $this->gtfsArchive->getShapesFile()->getShapePoints(), 'getShapeId', 6);
139 | self::assertEquals(2473, $shapeStartId);
140 |
141 | $shape = $this->gtfsArchive->getShapesFile()->getShape(6);
142 | self::assertEquals(1, count($shape));
143 | }
144 |
145 | public function testGetShape_shapeIdNotPresent()
146 | {
147 | $shape = $this->gtfsArchive->getShapesFile()->getShape(3);
148 | self::assertEquals(0, count($shape));
149 |
150 | $shape = $this->gtfsArchive->getShapesFile()->getShape(5);
151 | self::assertEquals(0, count($shape));
152 |
153 | // Additional verification of inner working, this should be in a separate test
154 | $shapeStartId = BinarySearchUtil::findIndexOfFirstFieldOccurrence(
155 | $this->gtfsArchive->getShapesFile()->getShapePoints(), 'getShapeId', 3);
156 | self::assertEquals(-1, $shapeStartId);
157 |
158 | $shapeStartId = BinarySearchUtil::findIndexOfFirstFieldOccurrence(
159 | $this->gtfsArchive->getShapesFile()->getShapePoints(), 'getShapeId', 5);
160 | self::assertEquals(-1, $shapeStartId);
161 | }
162 |
163 | public function testGetCalendar()
164 | {
165 | $calendar = $this->gtfsArchive->getCalendarFile()->getCalendarEntries();
166 | self::assertEquals(2, count($calendar));
167 | self::assertEquals(1, $calendar[0]->getServiceId());
168 | self::assertEquals(DateTime::createFromFormat("Ymd", "20190517"), $calendar[0]->getStartDate());
169 | self::assertEquals(DateTime::createFromFormat("Ymd", "20190614"), $calendar[0]->getEndDate());
170 | self::assertEquals(0, $calendar[0]->getMonday());
171 | self::assertEquals(0, $calendar[0]->getTuesday());
172 | self::assertEquals(0, $calendar[0]->getWednesday());
173 | self::assertEquals(0, $calendar[0]->getThursday());
174 | self::assertEquals(0, $calendar[0]->getFriday());
175 | self::assertEquals(0, $calendar[0]->getSaturday());
176 | self::assertEquals(0, $calendar[0]->getSunday());
177 | }
178 |
179 | public function testGetCalendarDates()
180 | {
181 | self::assertEquals(24, count($this->gtfsArchive->getCalendarDatesFile()->getCalendarDates()));
182 | }
183 |
184 | public function testGetCalendarDatesForService()
185 | {
186 | $dates = $this->gtfsArchive->getCalendarDatesFile()->getCalendarDatesForService(2);
187 | self::assertEquals(5, count($dates));
188 |
189 | self::assertEquals(2, $dates[0]->getServiceId());
190 | self::assertEquals(DateTime::createFromFormat("Ymd", 20190518), $dates[0]->getDate());
191 | self::assertEquals(1, $dates[0]->getExceptionType());
192 | }
193 |
194 | public function testGetTransfers()
195 | {
196 | $transfers = $this->gtfsArchive->getTransfersFile()->getTransfers();
197 | self::assertEquals(9, count($transfers));
198 | self::assertEquals("9021008001009000", $transfers[0]->getFromStopId());
199 | self::assertEquals("9021008001009000", $transfers[0]->getToStopId());
200 | self::assertEquals(2, $transfers[0]->getTransferType());
201 | self::assertEquals(0, $transfers[0]->getMinTransferTime());
202 | }
203 |
204 | public function testGetFeedInfo()
205 | {
206 | $feedinfo = $this->gtfsArchive->getFeedInfoFile()->getFeedInfo();
207 | self::assertEquals(1, count($feedinfo));
208 | self::assertEquals("Samtrafiken i Sverige AB", $feedinfo[0]->getFeedPublisherName());
209 | self::assertEquals("https://www.samtrafiken.se", $feedinfo[0]->getFeedPublisherUrl());
210 | self::assertEquals("2019-05-20", $feedinfo[0]->getFeedVersion());
211 | self::assertEquals("sv", $feedinfo[0]->getFeedLang());
212 | }
213 |
214 | public function testGetAgency()
215 | {
216 | $agencies = $this->gtfsArchive->getAgencyFile()->getAgencies();
217 | self::assertEquals(2, count($agencies));
218 | self::assertEquals(88100000000001375, $agencies[0]->getAgencyId());
219 | self::assertEquals("Kalmar Länstrafik", $agencies[0]->getAgencyName());
220 | self::assertEquals("https://www.resrobot.se/", $agencies[0]->getAgencyUrl());
221 | self::assertEquals("Europe/Stockholm", $agencies[0]->getAgencyTimezone());
222 | self::assertEquals("sv", $agencies[0]->getAgencyLang());
223 | self::assertNull($agencies[0]->getAgencyPhone(), 'Agency Phone is not null!');
224 | self::assertNull($agencies[1]->getAgencyEmail(), 'Agency Email is not null!');
225 | self::assertEquals('000-000-0000', $agencies[1]->getAgencyPhone(), 'Agency Phone is not equal to 000-000-0000');
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/Entities/Stop.php:
--------------------------------------------------------------------------------
1 | $value) {
39 | $this->$variable = isset($value) && $value !== '' ? $value : null;
40 | }
41 | $this->archive = $archive;
42 | }
43 |
44 | /**
45 | * Identifies a stop, station, or station entrance. The term "station entrance" refers to both station entrances
46 | * and station exits. Stops, stations, and station entrances are collectively referred to as locations. Multiple
47 | * routes can use the same stop.
48 | *
49 | * @return string
50 | */
51 | public function getStopId(): string
52 | {
53 | return $this->stop_id;
54 | }
55 |
56 | /**
57 | * Contains some short text or a number that uniquely identifies the stop for riders. Stop codes are often used in
58 | * phone-based transit information systems or printed on stop signage to make it easier for riders to get
59 | * information for a particular stop.
60 | *
61 | * The stop_code can be the same as stop_id if the ID is public facing. Leave this field blank for stops without a
62 | * code presented to riders.
63 | *
64 | * @return string | null
65 | */
66 | public function getStopCode(): ?string
67 | {
68 | return $this->stop_code;
69 | }
70 |
71 | /**
72 | * Contains the name of a location. Use a name that people understand in the local and tourist vernacular.
73 | *
74 | * When the location is a boarding area, with location_type=4, include the name of the boarding area as displayed
75 | * by the agency in the stop_name. It can be just one letter or text like "Wheelchair boarding area" or "Head of
76 | * short trains."
77 | *
78 | * This field is required for locations that are stops, stations, or entrances/exits, which have location_type
79 | * fields of 0, 1, and 2 respectively.
80 | *
81 | * This field is optional for locations that are generic nodes or boarding areas, which have location_type fields
82 | * of 3 and 4 respectively.
83 | *
84 | * @return string | null
85 | */
86 | public function getStopName(): ?string
87 | {
88 | return $this->stop_name;
89 | }
90 |
91 | /**
92 | * Describes a location. Provide useful, quality information. Don't simply duplicate the name of the location.
93 | *
94 | * @return string | null
95 | */
96 | public function getStopDesc(): ?string
97 | {
98 | return $this->stop_desc;
99 | }
100 |
101 | /**
102 | * Contains the latitude of a stop, station, or station entrance.
103 | *
104 | * This field is required for locations that are stops, stations, or entrances/exits, which have location_type
105 | * fields of 0, 1, and 2 respectively.
106 | *
107 | * This field is optional for locations that are generic nodes or boarding areas, which have location_type fields
108 | * of 3 and 4 respectively.
109 | *
110 | * @return float | null
111 | */
112 | public function getStopLat(): ?float
113 | {
114 | return floatval($this->stop_lat);
115 | }
116 |
117 | /**
118 | * Contains the longitude of a stop, station, or station entrance.
119 | *
120 | * This field is required for locations that are stops, stations, or entrances/exits, which have location_type
121 | * fields of 0, 1, and 2 respectively.
122 | *
123 | * This field is optional for locations that are generic nodes or boarding areas, which have location_type fields
124 | * of 3 and 4 respectively.
125 | *
126 | * @return float | null
127 | */
128 | public function getStopLon(): ?float
129 | {
130 | return floatval($this->stop_lon);
131 | }
132 |
133 | /**
134 | * Defines the fare zone for a stop. This field is required if you want to provide fare information with
135 | * fare_rules.txt. If this record represents a station or station entrance, the zone_id is ignored.
136 | *
137 | * @return string | null
138 | */
139 | public function getZoneId(): ?string
140 | {
141 | return $this->zone_id;
142 | }
143 |
144 | /**
145 | * Contains the URL of a web page about a particular stop. Make this different from the agency_url and route_url
146 | * fields.
147 | *
148 | * @return string | null
149 | */
150 | public function getStopUrl(): ?string
151 | {
152 | return $this->stop_url;
153 | }
154 |
155 | /**
156 | * For stops that are physically located inside stations, the parent_station field identifies the station
157 | * associated with the stop. Based on a combination of values for the parent_station and location_type fields, we
158 | * define three types of stops:
159 | *
160 | * A parent stop is an (often large) station or terminal building that can contain child stops.
161 | * This entry's location type is 1.
162 | * The parent_station field contains a blank value, because parent stops can't contain other parent stops.
163 | * A child stop is located inside of a parent stop. It can be an entrance, platform, node, or other pathway, as
164 | * defined in pathways.txt. This entry's location_type is 0 or (empty). The parent_station field contains the stop
165 | * ID of the station where this stop is located. The stop referenced in parent_station must have location_type=1. A
166 | * standalone stop is located outside of a parent stop. This entry's location type is 0 or (empty). The
167 | * parent_station field contains a blank value, because the parent_station field doesn't apply to this stop.
168 | *
169 | * @return string | null
170 | */
171 | public function getParentStation(): ?string
172 | {
173 | return $this->parent_station;
174 | }
175 |
176 | /**
177 | * Contains the timezone of this location. If omitted, it's assumed that the stop is located in the timezone
178 | * specified by the agency_timezone in agency.txt.
179 | *
180 | * When a stop has a parent station, the stop has the timezone specified by the parent station's stop_timezone
181 | * value. If the parent has no stop_timezone value, the stops that belong to that station are assumed to be in the
182 | * timezone specified by agency_timezone, even if the stops have their own stop_timezone values.
183 | *
184 | * In other words, if a given stop has a parent_station value, any stop_timezone value specified for that stop must
185 | * be ignored. Even if stop_timezone values are provided in stops.txt, continue to specify the times in
186 | * stop_times.txt relative to the timezone specified by the agency_timezone field in agency.txt. This ensures that
187 | * the time values in a trip always increase over the course of a trip, regardless of which timezones the trip
188 | * crosses.
189 | *
190 | * @return string | null
191 | */
192 | public function getStopTimezone(): ?string
193 | {
194 | return $this->stop_timezone;
195 | }
196 |
197 | /**
198 | * Provides the platform identifier for a platform stop, which is a stop that belongs to a station. Just include
199 | * the platform identifier, such as G or 3. Don't include words like "platform" or "track" or the feed's
200 | * language-specific equivalent. This allows feed consumers to more easily internationalize and localize the
201 | * platform identifier into other languages.
202 | *
203 | * @return string | null
204 | */
205 | public function getPlatformCode(): ?string
206 | {
207 | return $this->platform_code;
208 | }
209 |
210 | /**
211 | * Defines the type of the location. The location_type field can have the following values:
212 | *
213 | * 0 or (empty): Stop (or "Platform"). A location where passengers board or disembark from a transit vehicle. Stops
214 | * are called a "platform" when they're defined within a parent_station.
215 | * 1: Station. A physical structure or area that contains one or more platforms.
216 | * 2: Station entrance or exit. A location where passengers can enter or exit a station from the street. The stop
217 | * entry must also specify a parent_station value that references the stop_id of the parent station for the
218 | * entrance. If an entrance/exit belongs to multiple stations, it's linked by pathways to both, and the data
219 | * provider can either pick one station as parent, or put no parent station at all.
220 | * 3: Generic node. A location within a station that doesn't match any other location_type. Generic nodes are used
221 | * to link together the pathways defined in pathways.txt.
222 | * 4: Boarding area. A specific location on a platform where passengers can board or exit vehicles.
223 | *
224 | * @return int | null
225 | */
226 | public function getLocationType(): ?int
227 | {
228 | if ($this->location_type == null) {
229 | return null;
230 | }
231 | return intval($this->location_type);
232 | }
233 |
234 | /**
235 | * Identifies whether wheelchair boardings are possible from the specified stop, station, or station entrance. This
236 | * field can have the following values:
237 | *
238 | * 0 or (empty): Indicates that there's no accessibility information available for this stop.
239 | * 1: Indicates that at least some vehicles at this stop can be boarded by a rider in a wheelchair.
240 | * 2: Indicates that wheelchair boarding isn't possible at this stop.
241 | *
242 | * When a stop is part of a larger station complex, as indicated by the presence of a parent_station value, the
243 | * stop's wheelchair_boarding field has the following additional semantics:
244 | *
245 | * 0 or (empty): The stop inherits its wheelchair_boarding value from the parent station if it exists.
246 | * 1: Some accessible path exists from outside the station to the specific stop or platform.
247 | * 2: There are no accessible paths from outside the station to the specific stop or platform.
248 | *
249 | * For station entrances/exits, the wheelchair_boarding field has the following additional semantics:
250 | *
251 | * 0 or (empty): The station entrance inherits its wheelchair_boarding value from the parent station if it exists.
252 | * 1: The station entrance is wheelchair accessible, such as when an elevator is available to reach platforms that
253 | * aren't at-grade.
254 | * 2: There are no accessible paths from the entrance to the station platforms.
255 | *
256 | * @return int | null
257 | */
258 | public function getWheelchairBoarding(): ?int
259 | {
260 | return $this->wheelchair_boarding;
261 | }
262 |
263 | /**
264 | * Level of the location. The same level can be used by multiple unlinked stations.
265 | *
266 | * @return string | null
267 | */
268 | public function getLevelId(): ?string
269 | {
270 | return $this->level_id;
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/src/Trafiklab/Gtfs/Model/GtfsArchive.php:
--------------------------------------------------------------------------------
1 | fileRoot = $fileRoot;
56 | }
57 |
58 | /**
59 | * Download a GTFS zipfile.
60 | *
61 | * @param string $url The URL that points to the archive.
62 | *
63 | * @return GtfsArchive The downloaded archive.
64 | * @throws Exception
65 | */
66 | public static function createFromUrl(string $url): GtfsArchive
67 | {
68 | $downloadedArchive = self::downloadFile($url);
69 | $fileRoot = self::extractFiles($downloadedArchive, true);
70 | return new GtfsArchive($fileRoot);
71 | }
72 |
73 | /**
74 | * Creates the context and returns it for use in file_get_contents
75 | * Adds the If-Modified-Since header to check if the GTFS has been modified.
76 | */
77 | private static function createRequestContext(?\DateTime $lastModified = null)
78 | {
79 | self::$archiveLastModified = $lastModified !== null ? self::getLastModifiedFromDateTime($lastModified) : '';
80 | return stream_context_create([
81 | 'http' => [
82 | 'header' => "If-Modified-Since: " . self::$archiveLastModified,
83 | 'method' => 'GET',
84 | 'ignore_errors' => true
85 | ],
86 | ]);
87 | }
88 |
89 | /**
90 | * Check if the temp directory exists, if not create it.
91 | */
92 | private static function makeTempDir()
93 | {
94 | if (!file_exists(self::TEMP_ROOT)) {
95 | mkdir(self::TEMP_ROOT, 0777, true);
96 | }
97 | }
98 |
99 | /**
100 | * Only create a new Archive if it has been modified since the last PULL
101 | * We delete the Zip archive after each cycle as this is meant to constantly check headers.
102 | * @param string $url
103 | * @param \DateTime|null $lastModified
104 | * @param string|null $eTag
105 | * @return GtfsArchive|null
106 | * @throws Exception
107 | */
108 | public static function createFromUrlIfModified(
109 | string $url,
110 | ?\DateTime $lastModified = null,
111 | ?string $eTag = null
112 | ): ?GtfsArchive {
113 | $temp_file = self::TEMP_ROOT . md5($url) . ".zip";
114 | if (!file_exists($temp_file)) {
115 | try {
116 | /** Create the Request Context (Returns Stream Context) */
117 | $context = self::createRequestContext($lastModified);
118 | /** Make the temp directory if it doesn't exist. */
119 | self::makeTempDir();
120 |
121 | /** Download the zip file if it's been modified, or exists. */
122 | file_put_contents($temp_file, file_get_contents($url, false, $context));
123 |
124 | /**
125 | * @var array $http_response_header materializes out of thin air unfortunately
126 | * Parse the headers so we can retrieve what we need.
127 | */
128 | $responseHeaders = self::parseHeaders($http_response_header);
129 |
130 | /** @var integer $statusCode
131 | * Track the Status code to determine if the file has changed, or exists.
132 | */
133 | $statusCode = $responseHeaders['status'];
134 | switch ($statusCode) {
135 | case 200:
136 | /**
137 | * If the Status Code is 200, that means the file has been modified or it is a new GTFS Source.
138 | * Track the eTag and Last-Modified headers if they exist.
139 | */
140 | self::$archiveLastModified = $responseHeaders['last-modified'] ?? null;
141 | self::$archiveETag = $responseHeaders['etag'] ?? null;
142 |
143 | /** If no last-modified date is present, and Etag is present and matches, skip as it's not modified
144 | * If it returns 200 and LastModified is not null, that means that the Last-Modified date has changed.
145 | * Should always respect the Last-Modified date over eTag if there's a mismatch.
146 | */
147 | if (
148 | ($lastModified === null) &&
149 | ($eTag !== null && self::$archiveETag !== null && $eTag == self::$archiveETag)
150 | ) {
151 | return null;
152 | } else {
153 | /**
154 | * Last-Modified Date wasn't present, and eTag is not present or didn't match.
155 | * Extract files and return the GtfsArchive.
156 | */
157 | $fileRoot = self::extractFiles($temp_file, true);
158 | return new GtfsArchive($fileRoot);
159 | }
160 | case 304:
161 | /** 304 = NOT_MODIFIED (This file hasn't changed since the last pull) */
162 | self::$archiveETag = $eTag;
163 | break;
164 | default:
165 | /** Status Code returned a 400 error or similar meaning the URL is invalid or down. */
166 | throw new Exception("Could not open the GTFS archive, Status Code: {$statusCode}");
167 | }
168 | } catch (Exception $exception) {
169 | throw new Exception(
170 | "There was an issue downloading the GTFS from the requested URL: {$url}, Error: " .
171 | $exception->getMessage(), $statusCode ?? 0
172 | );
173 | } finally {
174 | /** Clean up - Delete the Downloaded Zip if it exists. */
175 | self::deleteArchive($temp_file);
176 | }
177 | }
178 | return null;
179 | }
180 |
181 | /**
182 | * Open a local GTFS zipfile.
183 | *
184 | * @param string $path The path that points to the archive.
185 | *
186 | * @return GtfsArchive The downloaded archive.
187 | * @throws Exception
188 | */
189 | public static function createFromPath(string $path): GtfsArchive
190 | {
191 | $fileRoot = self::extractFiles($path);
192 | return new GtfsArchive($fileRoot);
193 | }
194 |
195 | /**
196 | * Download and extract the latest GTFS data set
197 | *
198 | * @param string $url
199 | *
200 | * @return string
201 | */
202 | private static function downloadFile(string $url): string
203 | {
204 | $temp_file = self::TEMP_ROOT . md5($url) . ".zip";
205 |
206 | if (!file_exists($temp_file)) {
207 | // Download zip file with GTFS data.
208 | file_put_contents($temp_file, file_get_contents($url));
209 | }
210 |
211 | return $temp_file;
212 | }
213 |
214 | /**
215 | * @throws Exception
216 | */
217 | private static function extractFiles(string $archiveFilePath, bool $deleteArchive = false)
218 | {
219 | // Load the zip file.
220 | $zip = new ZipArchive();
221 | if ($zip->open($archiveFilePath) != 'true') {
222 | throw new Exception('Could not open the GTFS archive');
223 | }
224 | // Extract the zip file and remove it.
225 | $extractionPath = substr($archiveFilePath, 0, strlen($archiveFilePath) - 4) . '/';
226 | $zip->extractTo($extractionPath);
227 | $zip->close();
228 |
229 | if ($deleteArchive) {
230 | self::deleteArchive($archiveFilePath);
231 | }
232 |
233 | return $extractionPath;
234 | }
235 |
236 | /**
237 | * @return GtfsAgencyFile
238 | */
239 | public function getAgencyFile(): GtfsAgencyFile
240 | {
241 | return $this->loadGtfsFileThroughCache(__METHOD__, self::AGENCY_TXT, GtfsAgencyFile::class);
242 | }
243 |
244 | /**
245 | * @return GtfsCalendarDatesFile
246 | */
247 | public function getCalendarDatesFile(): GtfsCalendarDatesFile
248 | {
249 | return $this->loadGtfsFileThroughCache(__METHOD__, self::CALENDAR_DATES_TXT, GtfsCalendarDatesFile::class);
250 | }
251 |
252 | /**
253 | * @return GtfsCalendarFile
254 | */
255 | public function getCalendarFile(): GtfsCalendarFile
256 | {
257 | return $this->loadGtfsFileThroughCache(__METHOD__, self::CALENDAR_TXT, GtfsCalendarFile::class);
258 | }
259 |
260 | /**
261 | * @return GtfsFeedInfoFile
262 | */
263 | public function getFeedInfoFile(): GtfsFeedInfoFile
264 | {
265 | return $this->loadGtfsFileThroughCache(__METHOD__, self::FEED_INFO_TXT, GtfsFeedInfoFile::class);
266 | }
267 |
268 | /**
269 | * @return GtfsRoutesFile
270 | */
271 | public function getRoutesFile(): GtfsRoutesFile
272 | {
273 | return $this->loadGtfsFileThroughCache(__METHOD__, self::ROUTES_TXT, GtfsRoutesFile::class);
274 | }
275 |
276 | /**
277 | * @return GtfsShapesFile
278 | */
279 | public function getShapesFile(): GtfsShapesFile
280 | {
281 | return $this->loadGtfsFileThroughCache(__METHOD__, self::SHAPES_TXT, GtfsShapesFile::class);
282 | }
283 |
284 | /**
285 | * @return GtfsStopsFile
286 | */
287 | public function getStopsFile(): GtfsStopsFile
288 | {
289 | return $this->loadGtfsFileThroughCache(__METHOD__, self::STOPS_TXT, GtfsStopsFile::class);
290 | }
291 |
292 | /**
293 | * @return GtfsStopTimesFile
294 | */
295 | public function getStopTimesFile(): GtfsStopTimesFile
296 | {
297 | return $this->loadGtfsFileThroughCache(__METHOD__, self::STOP_TIMES_TXT, GtfsStopTimesFile::class);
298 | }
299 |
300 | /**
301 | * @return GtfsTransfersFile
302 | */
303 | public function getTransfersFile(): GtfsTransfersFile
304 | {
305 | return $this->loadGtfsFileThroughCache(__METHOD__, self::TRANSFERS_TXT, GtfsTransfersFile::class);
306 | }
307 |
308 | /**
309 | * @return GtfsTripsFile
310 | */
311 | public function getTripsFile(): GtfsTripsFile
312 | {
313 | return $this->loadGtfsFileThroughCache(__METHOD__, self::TRIPS_TXT, GtfsTripsFile::class);
314 | }
315 |
316 | /**
317 | * @return GtfsFrequenciesFile
318 | */
319 | public function getFrequenciesFile(): GtfsFrequenciesFile
320 | {
321 | return $this->loadGtfsFileThroughCache(__METHOD__, self::FREQUENCIES_TXT, GtfsFrequenciesFile::class);
322 | }
323 |
324 | /**
325 | * Delete the uncompressed files. This should be done as a cleanup when you're ready.
326 | */
327 | public function deleteUncompressedFiles()
328 | {
329 | // Remove temporary data.
330 | if (!file_exists($this->fileRoot)) {
331 | return;
332 | }
333 | $files = scandir($this->fileRoot);
334 | foreach ($files as $file) {
335 | if ($file != '.' && $file != '..') {
336 | $path = $this->fileRoot . DIRECTORY_SEPARATOR . $file;
337 | /** Check for OS Specific directories that could've been added. Ex: _MACOSX/ */
338 | if (is_dir($path)) {
339 | rmdir($path);
340 | } else {
341 | // Remove all extracted files from the zip file.
342 | unlink($path);
343 | }
344 |
345 | }
346 | }
347 | reset($files);
348 | // Remove the empty folder.
349 | rmdir($this->fileRoot);
350 | }
351 |
352 | private static function deleteArchive(string $archiveFilePath)
353 | {
354 | if (file_exists($archiveFilePath)) {
355 | unlink($archiveFilePath);
356 | }
357 | }
358 |
359 | private function loadGtfsFileThroughCache(string $method, string $file, string $class)
360 | {
361 | if ($this->getCachedResult($method) == null) {
362 | $this->setCachedResult($method, new $class($this, $this->fileRoot . $file));
363 | }
364 | return $this->getCachedResult($method);
365 | }
366 |
367 | /**
368 | * Parse file_get_contents Response headers into an array
369 | * Primarily to retrieve the Status Code, Last-Modified, and ETag headers.
370 | * @param array $headers
371 | * @return array
372 | */
373 | private static function parseHeaders(array $headers): array
374 | {
375 | $headersArray = [];
376 | foreach($headers as $k => $v) {
377 | $t = explode(':', $v, 2);
378 | if (isset($t[1])) {
379 | $headersArray[ strtolower(trim($t[0])) ] = trim( $t[1] );
380 | }
381 | else {
382 | $headersArray[] = $v;
383 | if (preg_match("#HTTP/[0-9\.]+\s+([0-9]+)#", $v, $out) )
384 | $headersArray['status'] = intval($out[1]);
385 | }
386 | }
387 | return $headersArray;
388 | }
389 |
390 | /**
391 | * Return the DateTime Object for the Last Modified Date
392 | * Useful for storing in databases, etc.
393 | * @return \DateTime|null
394 | * @throws Exception
395 | */
396 | public function getLastModifiedDateTime(): ?\DateTime
397 | {
398 | $datetime = (
399 | new \DateTime(
400 | self::$archiveLastModified,
401 | new \DateTimeZone('GMT')
402 | )
403 | );
404 | return self::$archiveLastModified !== null ? $datetime : null;
405 | }
406 |
407 | /**
408 | * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
409 | * @param \DateTime $dateTime
410 | * @return string
411 | */
412 | public static function getLastModifiedFromDateTime(\DateTime $dateTime): string
413 | {
414 | return $dateTime
415 | ->setTimezone(new \DateTimeZone('GMT'))
416 | ->format(self::LAST_MODIFIED_FORMAT);
417 | }
418 |
419 | /**
420 | * @return string|null
421 | */
422 | public function getArchiveLastModified(): ?string
423 | {
424 | return self::$archiveLastModified ?? null;
425 | }
426 |
427 | /**
428 | * @return string|null
429 | */
430 | public function getArchiveETag(): ?string
431 | {
432 | return self::$archiveETag ?? null;
433 | }
434 | }
435 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------