├── .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 | [![Build status](https://travis-ci.com/trafiklab/gtfs-php-sdk.svg?branch=master)](https://travis-ci.com/trafiklab/gtfs-php-sdk) 4 | [![Latest Stable Version](https://poser.pugx.org/trafiklab/gtfs-php-sdk/v/stable)](https://packagist.org/packages/trafiklab/gtfs-php-sdk) 5 | [![codecov](https://codecov.io/gh/trafiklab/gtfs-php-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/trafiklab/gtfs-php-sdk) 6 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](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 | --------------------------------------------------------------------------------