├── gtfs_realtime_translators ├── __version__.py ├── factories │ ├── __init__.py │ └── factories.py ├── validators │ ├── __init__.py │ └── validators.py ├── registry │ ├── __init__.py │ └── registry.py ├── translators │ ├── __init__.py │ ├── la_metro.py │ ├── wcdot_bus.py │ ├── cta_bus.py │ ├── mta_subway.py │ ├── njt_bus_json.py │ ├── mnmt.py │ ├── swiftly.py │ ├── cta_subway.py │ ├── marta_rail.py │ ├── septa_regional_rail.py │ ├── njt_bus.py │ ├── path_rail.py │ ├── path_rail_new.py │ ├── path_new.py │ ├── mbta.py │ ├── njt_rail_json.py │ └── njt_rail.py └── bindings │ ├── intersection.proto │ └── intersection_pb2.py ├── requirements.txt ├── .travis.yml ├── test ├── fixtures │ ├── cta_bus.json │ ├── mnmt.json │ ├── cta_subway.json │ ├── vta_rail.json │ ├── septa_trolley_lines.json │ ├── la_metro_rail.json │ ├── njt_bus.xml │ ├── mbta_subway_missing_static.json │ ├── path_rail.json │ ├── mbta_subway.json │ └── mta_subway.json ├── test_path_rail.py ├── test_wcdot_bus.py ├── test_mta_subway.py ├── test_path_new.py ├── test_vta.py ├── test_njt_bus.py ├── test_registry.py ├── test_cta_bus.py ├── test_cta_subway.py ├── test_la_metro.py ├── test_septa.py ├── test_trip_update.py ├── test_mnmt.py ├── test_septa_regional_rail.py ├── test_njt_rail.py └── test_mbta.py ├── .github └── workflows │ └── codeql.yml ├── setup.py ├── .gitignore ├── README.md └── LICENSE /gtfs_realtime_translators/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.0' 2 | __license__ = "Apache-2.0" 3 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/factories/__init__.py: -------------------------------------------------------------------------------- 1 | from .factories import FeedMessage, TripUpdate 2 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/validators/__init__.py: -------------------------------------------------------------------------------- 1 | from .validators import RequiredFieldValidator 2 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/registry/__init__.py: -------------------------------------------------------------------------------- 1 | from .registry import TranslatorRegistry, TranslatorKeyWarning 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gtfs-realtime-bindings==1.0.0 2 | pendulum==2.0.5 3 | pytest==5.2.2 4 | xmltodict==0.12.0 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | # command to install dependencies 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install -e . 10 | # command to run tests 11 | script: 12 | - pytest -------------------------------------------------------------------------------- /gtfs_realtime_translators/validators/validators.py: -------------------------------------------------------------------------------- 1 | class RequiredFieldValidator: 2 | @staticmethod 3 | def validate_field_value(fieldName, fieldValue): 4 | if fieldValue is None: 5 | raise ValueError('{} is required.'.format(fieldName)) 6 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/__init__.py: -------------------------------------------------------------------------------- 1 | from .la_metro import LaMetroGtfsRealtimeTranslator 2 | from .septa_regional_rail import SeptaRegionalRailTranslator 3 | from .mta_subway import MtaSubwayGtfsRealtimeTranslator 4 | from .njt_rail import NjtRailGtfsRealtimeTranslator 5 | from .njt_rail_json import NjtRailJsonGtfsRealtimeTranslator 6 | from .njt_bus import NjtBusGtfsRealtimeTranslator 7 | from .njt_bus_json import NjtBusJsonGtfsRealtimeTranslator 8 | from .cta_subway import CtaSubwayGtfsRealtimeTranslator 9 | from .cta_bus import CtaBusGtfsRealtimeTranslator 10 | from .path_rail import PathGtfsRealtimeTranslator 11 | from .path_new import PathNewGtfsRealtimeTranslator 12 | from .swiftly import SwiftlyGtfsRealtimeTranslator 13 | from .wcdot_bus import WcdotGtfsRealTimeTranslator 14 | from .mbta import MbtaGtfsRealtimeTranslator 15 | from .mnmt import MnmtGtfsRealtimeTranslator 16 | from .marta_rail import MartaRailGtfsRealtimeTranslator 17 | from .path_rail_new import PathRailNewGtfsRealtimeTranslator 18 | -------------------------------------------------------------------------------- /test/fixtures/cta_bus.json: -------------------------------------------------------------------------------- 1 | { 2 | "bustime-response": { 3 | "prd": [ 4 | { 5 | "tmstmp": "20191008 10:34", 6 | "typ": "A", 7 | "stpnm": "Sheridan & Arthur (Red Line)", 8 | "stpid": "1203", 9 | "vid": "4379", 10 | "dstp": 2603, 11 | "rt": "147", 12 | "rtdd": "147", 13 | "rtdir": "Northbound", 14 | "des": "Howard Station", 15 | "prdtm": "20191008 10:38", 16 | "tablockid": "147 -501", 17 | "tatripid": "1063847", 18 | "dly": false, 19 | "prdctdn": "3", 20 | "zone": "" 21 | }, 22 | { 23 | "tmstmp": "20191008 10:26", 24 | "typ": "D", 25 | "stpnm": "Sheridan & Arthur (Red Line)", 26 | "stpid": "1203", 27 | "vid": "1396", 28 | "dstp": 13893, 29 | "rt": "155", 30 | "rtdd": "155", 31 | "rtdir": "Eastbound", 32 | "des": "Morse Red Line", 33 | "prdtm": "20191008 10:45", 34 | "tablockid": "155 -502", 35 | "tatripid": "1005547", 36 | "dly": false, 37 | "prdctdn": "10", 38 | "zone": "" 39 | } 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "53 18 * * 4" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /test/test_path_rail.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | import pytest 3 | 4 | from gtfs_realtime_translators.factories import FeedMessage 5 | from gtfs_realtime_translators.translators.path_rail import PathGtfsRealtimeTranslator 6 | 7 | 8 | @pytest.fixture 9 | def path_rail(): 10 | with open('test/fixtures/path_rail.json') as f: 11 | raw = f.read() 12 | return raw 13 | 14 | 15 | def test_path_data(path_rail): 16 | translator = PathGtfsRealtimeTranslator() 17 | with pendulum.test(pendulum.datetime(2020, 2, 22, 12, 0, 0)): 18 | message = translator(path_rail) 19 | 20 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 21 | 22 | entity = message.entity[0] 23 | trip_update = entity.trip_update 24 | assert trip_update.trip.trip_id == '' 25 | assert trip_update.trip.route_id == '861' 26 | 27 | stop_time_update = trip_update.stop_time_update[0] 28 | assert stop_time_update.stop_id == '781741' 29 | assert stop_time_update.departure.time == 1582372920 30 | assert stop_time_update.arrival.time == 1582372920 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from setuptools import setup 4 | 5 | PACKAGE_ROOT = 'gtfs_realtime_translators' 6 | 7 | with open('README.md') as readme_file: 8 | readme = readme_file.read() 9 | 10 | about = dict() 11 | with io.open(f'{PACKAGE_ROOT}/__version__.py', 'r', encoding='utf-8') as f: 12 | exec(f.read(), about) 13 | 14 | requirements = [ 15 | 'gtfs-realtime-bindings==1.0.0', 16 | 'pendulum==2.0.5', 17 | 'xmltodict==0.12.0', 18 | ] 19 | 20 | setup( 21 | name="gtfs-realtime-translators", 22 | version=about['__version__'], 23 | description='Translating custom arrivals formats to GTFS-realtime.', 24 | long_description=readme, 25 | long_description_content_type='text/markdown', 26 | author='Tyler Green', 27 | author_email='tyler.green@intersection.com', 28 | url='https://github.com/Intersection/gtfs-realtime-translators', 29 | packages=[ 30 | f'{PACKAGE_ROOT}.translators', 31 | f'{PACKAGE_ROOT}.factories', 32 | f'{PACKAGE_ROOT}.registry', 33 | f'{PACKAGE_ROOT}.bindings', 34 | f'{PACKAGE_ROOT}.validators', 35 | ], 36 | license=about['__license__'], 37 | install_requires=requirements, 38 | ) 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .nox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *.cover 44 | .hypothesis/ 45 | .pytest_cache/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # PyBuilder 52 | target/ 53 | 54 | # Jupyter Notebook 55 | .ipynb_checkpoints 56 | 57 | # IPython 58 | profile_default/ 59 | ipython_config.py 60 | 61 | # pyenv 62 | .python-version 63 | 64 | # celery beat schedule file 65 | celerybeat-schedule 66 | 67 | # Environments 68 | .env 69 | .venv 70 | env/ 71 | venv/ 72 | ENV/ 73 | env.bak/ 74 | venv.bak/ 75 | 76 | # mkdocs documentation 77 | /site 78 | 79 | # IDE 80 | .idea 81 | *.iml 82 | -------------------------------------------------------------------------------- /test/test_wcdot_bus.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gtfs_realtime_translators.translators import WcdotGtfsRealTimeTranslator 4 | from gtfs_realtime_translators.factories import FeedMessage 5 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 6 | 7 | @pytest.fixture 8 | def wcdot_bus(): 9 | with open('test/fixtures/wcdot_bus.json') as f: 10 | raw = f.read() 11 | 12 | return raw 13 | 14 | def test_wcdot_data(wcdot_bus): 15 | translator = WcdotGtfsRealTimeTranslator(stop_id='5142') 16 | message = translator(wcdot_bus) 17 | entity = message.entity[0] 18 | trip_update = entity.trip_update 19 | stop_time_update = trip_update.stop_time_update[0] 20 | assert trip_update.trip.trip_id == '8612' 21 | assert stop_time_update.arrival.delay == -60 22 | assert stop_time_update.departure.delay == -60 23 | assert trip_update.trip.route_id == "66" 24 | assert entity.id == "130" 25 | assert stop_time_update.stop_id == "5142" 26 | 27 | def test_invalid_stop_id(wcdot_bus): 28 | translator = WcdotGtfsRealTimeTranslator(stop_id='99999') 29 | message = translator(wcdot_bus) 30 | entity = message.entity 31 | assert len(entity) == 0 -------------------------------------------------------------------------------- /gtfs_realtime_translators/bindings/intersection.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "gtfs-realtime.proto"; 4 | 5 | message IntersectionTripUpdate { 6 | optional string headsign = 1; 7 | optional string route_short_name = 2; 8 | optional string route_long_name = 3; 9 | optional string route_color = 4; 10 | optional string route_text_color = 5; 11 | optional string block_id = 6; 12 | optional string agency_timezone = 7; 13 | optional string custom_status = 8; 14 | optional int32 scheduled_interval = 9; 15 | optional string route_icon = 10; 16 | } 17 | 18 | message IntersectionStopTimeUpdate { 19 | optional string track = 1; 20 | optional transit_realtime.TripUpdate.StopTimeEvent scheduled_arrival = 2; 21 | optional transit_realtime.TripUpdate.StopTimeEvent scheduled_departure = 3; 22 | optional string stop_name = 4; 23 | } 24 | 25 | extend transit_realtime.TripUpdate { 26 | optional IntersectionTripUpdate intersection_trip_update = 1987; 27 | } 28 | 29 | extend transit_realtime.TripUpdate.StopTimeUpdate { 30 | optional IntersectionStopTimeUpdate intersection_stop_time_update = 1987; 31 | } 32 | 33 | 34 | message IntersectionVehicleDescriptor { 35 | optional string run_number = 1; 36 | } 37 | 38 | extend transit_realtime.VehicleDescriptor { 39 | optional IntersectionVehicleDescriptor intersection_vehicle_descriptor = 1987; 40 | } 41 | -------------------------------------------------------------------------------- /test/test_mta_subway.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import MtaSubwayGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 6 | from gtfs_realtime_translators.factories import FeedMessage 7 | 8 | 9 | @pytest.fixture 10 | def mta_subway(): 11 | with open('test/fixtures/mta_subway.json') as f: 12 | raw = f.read() 13 | 14 | return raw 15 | 16 | 17 | def test_mta_subway_data(mta_subway): 18 | translator = MtaSubwayGtfsRealtimeTranslator() 19 | with pendulum.test(pendulum.datetime(2019,2,20,17,0,0)): 20 | message = translator(mta_subway) 21 | 22 | entity = message.entity[0] 23 | trip_update = entity.trip_update 24 | stop_time_update = trip_update.stop_time_update[0] 25 | 26 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 27 | 28 | assert entity.id == '1' 29 | 30 | assert stop_time_update.arrival.time == 1569507335 31 | assert stop_time_update.departure.time == 1569507335 32 | assert stop_time_update.stop_id == '101N' 33 | assert entity.trip_update.trip.route_id == '1' 34 | assert entity.trip_update.trip.trip_id == '2351' 35 | 36 | assert stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].stop_name == 'Van Cortlandt Park - 242 St' 37 | -------------------------------------------------------------------------------- /test/test_path_new.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | import pytest 3 | 4 | from gtfs_realtime_translators.factories import FeedMessage 5 | from gtfs_realtime_translators.translators.path_new import PathNewGtfsRealtimeTranslator 6 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 7 | 8 | 9 | @pytest.fixture 10 | def path_new(): 11 | with open('test/fixtures/path_new.json') as f: 12 | raw = f.read() 13 | return raw 14 | 15 | 16 | def test_path_data(path_new): 17 | translator = PathNewGtfsRealtimeTranslator() 18 | with pendulum.test(pendulum.datetime(2020, 2, 22, 12, 0, 0)): 19 | message = translator(path_new) 20 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 21 | entity = message.entity[0] 22 | assert entity.id == '15:55_NWK/WTC' 23 | trip_update = entity.trip_update 24 | assert trip_update.trip.trip_id == '' 25 | assert trip_update.trip.route_id == '862' 26 | stop_time_update = trip_update.stop_time_update[0] 27 | assert stop_time_update.stop_id == '781718' 28 | assert stop_time_update.departure.time == 1635796500 29 | assert stop_time_update.arrival.time == 1635796500 30 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 31 | intersection_stop_time_update = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 32 | assert intersection_stop_time_update.track == "Track H" 33 | assert intersection_stop_time_update.stop_name == "Newark" 34 | assert intersection_trip_update.headsign == "World Trade Center" 35 | -------------------------------------------------------------------------------- /test/fixtures/mnmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "stops": [ 3 | { 4 | "stop_id": 11191, 5 | "latitude": 45.013401, 6 | "longitude": -93.287947, 7 | "description": "Lyndale Ave N & Lowry Ave N" 8 | } 9 | ], 10 | "alerts": [ 11 | { 12 | "stop_closed": false, 13 | "alert_text": "The following stop is closed for Routes 3, 7 and 22 until further notice: Washington Ave S & Park Ave - Stop #19306 (westbound)" 14 | } 15 | ], 16 | "departures": [ 17 | { 18 | "actual": true, 19 | "trip_id": "24557038-DEC23-MVS-BUS-Weekday-03", 20 | "stop_id": 11191, 21 | "departure_text": "14 Min", 22 | "departure_time": 1702923655, 23 | "description": "Brklyn Ctr Tc / N Lyndale / Via Penn Av", 24 | "route_id": "22", 25 | "route_short_name": "22", 26 | "direction_id": 0, 27 | "direction_text": "NB", 28 | "terminal": "A", 29 | "schedule_relationship": "Scheduled" 30 | }, 31 | { 32 | "actual": false, 33 | "trip_id": "24557032-DEC23-MVS-BUS-Weekday-03", 34 | "stop_id": 11191, 35 | "departure_text": "12:42", 36 | "departure_time": 1702924920, 37 | "description": "Brklyn Ctr Tc / N Lyndale / Via Humboldt", 38 | "route_id": "22", 39 | "route_short_name": "22", 40 | "direction_id": 0, 41 | "direction_text": "NB", 42 | "schedule_relationship": "Scheduled" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /test/test_vta.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import SwiftlyGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.factories import FeedMessage 6 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 7 | 8 | 9 | @pytest.fixture 10 | def vta_rail(): 11 | with open('test/fixtures/vta_rail.json') as f: 12 | raw = f.read() 13 | 14 | return raw 15 | 16 | 17 | def test_vta_data(vta_rail): 18 | translator = SwiftlyGtfsRealtimeTranslator(stop_id='5236') 19 | with pendulum.test(pendulum.datetime(2019,2,20,17,0,0)): 20 | message = translator(vta_rail) 21 | 22 | entity = message.entity[0] 23 | trip_update = entity.trip_update 24 | stop_time_update = trip_update.stop_time_update[0] 25 | 26 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 27 | 28 | assert entity.id == '1' 29 | 30 | assert trip_update.trip.trip_id == '2960461' 31 | assert trip_update.trip.route_id == 'Ornge' 32 | 33 | assert stop_time_update.arrival.time == 1550682060 34 | assert stop_time_update.departure.time == 1550682060 35 | assert stop_time_update.stop_id == '5236' 36 | 37 | # test extensions 38 | ixn_stop_time_updates = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 39 | ixn_trip_updates = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 40 | assert ixn_stop_time_updates.stop_name == "Milpitas Station" 41 | assert ixn_trip_updates.headsign == "Alum Rock" 42 | assert ixn_trip_updates.route_short_name == "Orange Line" 43 | assert ixn_trip_updates.route_long_name == "Orange Line - Mountain View - Alum Rock" 44 | -------------------------------------------------------------------------------- /test/fixtures/cta_subway.json: -------------------------------------------------------------------------------- 1 | { 2 | "ctatt": { 3 | "tmst": "2019-10-07T14:29:33", 4 | "errCd": "0", 5 | "errNm": null, 6 | "eta": [ 7 | { 8 | "staId": "41300", 9 | "stpId": "30251", 10 | "staNm": "Loyola", 11 | "stpDe": "Service toward Howard", 12 | "rn": "806", 13 | "rt": "Red", 14 | "destSt": "30173", 15 | "destNm": "Howard", 16 | "trDr": "1", 17 | "prdt": "2019-10-07T14:29:02", 18 | "arrT": "2019-10-07T14:30:02", 19 | "isApp": "1", 20 | "isSch": "0", 21 | "schInt": "0", 22 | "isDly": "0", 23 | "isFlt": "0", 24 | "flags": null, 25 | "lat": "41.99673", 26 | "lon": "-87.65923", 27 | "heading": "357" 28 | }, 29 | { 30 | "staId": "41300", 31 | "stpId": "30251", 32 | "staNm": "Loyola", 33 | "stpDe": "Service toward Howard", 34 | "rn": "824", 35 | "rt": "Red", 36 | "destSt": "30173", 37 | "destNm": "Howard", 38 | "trDr": "1", 39 | "prdt": "2019-10-07T14:28:56", 40 | "arrT": "2019-10-07T14:32:56", 41 | "isApp": "0", 42 | "isSch": "1", 43 | "schInt": "1", 44 | "isDly": "0", 45 | "isFlt": "0", 46 | "flags": null, 47 | "lat": "41.9835", 48 | "lon": "-87.65884", 49 | "heading": "358" 50 | } 51 | ] 52 | } 53 | } -------------------------------------------------------------------------------- /test/fixtures/vta_rail.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "route": "/real-time/vta/predictions GET", 4 | "data": { 5 | "agencyKey": "vta", 6 | "predictionsData": [ 7 | { 8 | "routeShortName": "Orange Line", 9 | "routeName": "Orange Line - Mountain View - Alum Rock", 10 | "routeId": "Ornge", 11 | "stopId": "5236", 12 | "stopName": "Milpitas Station", 13 | "stopCode": 65236, 14 | "destinations": [ 15 | { 16 | "directionId": "0", 17 | "headsign": "Alum Rock", 18 | "predictions": [ 19 | { 20 | "time": 1600886400, 21 | "sec": 86, 22 | "min": 1, 23 | "departure": true, 24 | "tripId": "2960461", 25 | "vehicleId": "937" 26 | }, 27 | { 28 | "time": 1600887600, 29 | "sec": 1286, 30 | "min": 21, 31 | "departure": true, 32 | "tripId": "2960460", 33 | "vehicleId": "919" 34 | }, 35 | { 36 | "time": 1600888800, 37 | "sec": 2486, 38 | "min": 41, 39 | "departure": true, 40 | "tripId": "2960459", 41 | "vehicleId": "987" 42 | }, 43 | { 44 | "time": 1600890000, 45 | "sec": 3686, 46 | "min": 61, 47 | "departure": true, 48 | "tripId": "2960458", 49 | "vehicleId": "979" 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/la_metro.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | 4 | import pendulum 5 | 6 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 7 | from gtfs_realtime_translators.validators import RequiredFieldValidator 8 | 9 | 10 | class LaMetroGtfsRealtimeTranslator: 11 | TIMEZONE = 'America/Los_Angeles' 12 | 13 | def __init__(self, stop_id=None): 14 | RequiredFieldValidator.validate_field_value('stop_id', stop_id) 15 | self.stop_id = stop_id 16 | 17 | def __call__(self, data): 18 | json_data = json.loads(data) 19 | entities = [ self.__make_trip_update(idx, self.stop_id, arrival) for idx, arrival in enumerate(json_data['items']) ] 20 | return FeedMessage.create(entities=entities) 21 | 22 | @classmethod 23 | def calculate_trip_id(cls, trip_id): 24 | """ 25 | Trip IDs from LA Metro often come in the form _. 26 | We must parse the static_trip_id only. 27 | """ 28 | try: 29 | return trip_id.split('_')[0] 30 | except Exception: 31 | return trip_id 32 | 33 | @classmethod 34 | def __make_trip_update(cls, _id, stop_id, arrival): 35 | entity_id = str(_id + 1) 36 | now = int(pendulum.now().timestamp()) 37 | arrival_time = now + math.floor(arrival['seconds'] / 60) * 60 38 | trip_id = cls.calculate_trip_id(arrival['trip_id']) 39 | route_id = arrival.get('route_id','') 40 | 41 | return TripUpdate.create(entity_id=entity_id, 42 | arrival_time=arrival_time, 43 | trip_id=trip_id, 44 | route_id=route_id, 45 | stop_id=stop_id, 46 | agency_timezone=cls.TIMEZONE) 47 | -------------------------------------------------------------------------------- /test/test_njt_bus.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gtfs_realtime_translators.factories import FeedMessage 4 | from gtfs_realtime_translators.translators import NjtBusGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 6 | 7 | 8 | @pytest.fixture 9 | def njt_bus(): 10 | with open('test/fixtures/njt_bus.xml') as f: 11 | raw = f.read() 12 | return raw 13 | 14 | 15 | def test_njt_data(njt_bus): 16 | translator = NjtBusGtfsRealtimeTranslator(stop_list = "2916, 39787") 17 | message = translator(njt_bus) 18 | 19 | entity = message.entity[0] 20 | trip_update = entity.trip_update 21 | 22 | stop_time_update = trip_update.stop_time_update[0] 23 | 24 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 25 | assert entity.id == '1' 26 | assert trip_update.trip.route_id == '165' 27 | assert trip_update.trip.trip_id == '22899' 28 | 29 | assert stop_time_update.stop_id == '2916' 30 | 31 | assert stop_time_update.departure.time == 1625142660 32 | assert stop_time_update.arrival.time == 1625142660 33 | 34 | intersection_trip_update = trip_update.Extensions[ 35 | intersection_gtfs_realtime.intersection_trip_update] 36 | assert intersection_trip_update.headsign == 'Weehawken Via Journal Sq' 37 | assert intersection_trip_update.agency_timezone == 'America/New_York' 38 | 39 | intersection_stop_time_update = stop_time_update.Extensions[ 40 | intersection_gtfs_realtime.intersection_stop_time_update] 41 | assert intersection_stop_time_update.scheduled_arrival.time == 1625143140 42 | assert intersection_stop_time_update.scheduled_departure.time == 1625143140 43 | assert intersection_stop_time_update.stop_name == 'Journal Square Transportation Center' 44 | assert intersection_stop_time_update.track == 'C-1' 45 | -------------------------------------------------------------------------------- /test/test_registry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gtfs_realtime_translators.translators import LaMetroGtfsRealtimeTranslator, \ 4 | SeptaRegionalRailTranslator, \ 5 | SwiftlyGtfsRealtimeTranslator, \ 6 | CtaSubwayGtfsRealtimeTranslator, \ 7 | CtaBusGtfsRealtimeTranslator, \ 8 | MtaSubwayGtfsRealtimeTranslator, \ 9 | NjtRailGtfsRealtimeTranslator, \ 10 | NjtBusGtfsRealtimeTranslator, \ 11 | PathGtfsRealtimeTranslator, \ 12 | PathNewGtfsRealtimeTranslator, \ 13 | WcdotGtfsRealTimeTranslator, \ 14 | MbtaGtfsRealtimeTranslator 15 | from gtfs_realtime_translators.registry import TranslatorRegistry, TranslatorKeyWarning 16 | 17 | def test_registry_for_valid_key(): 18 | assert TranslatorRegistry.get('la-metro-old') == LaMetroGtfsRealtimeTranslator 19 | assert TranslatorRegistry.get('septa-regional-rail') == SeptaRegionalRailTranslator 20 | assert TranslatorRegistry.get('cta-subway') == CtaSubwayGtfsRealtimeTranslator 21 | assert TranslatorRegistry.get('cta-bus') == CtaBusGtfsRealtimeTranslator 22 | assert TranslatorRegistry.get('mta-subway') == MtaSubwayGtfsRealtimeTranslator 23 | assert TranslatorRegistry.get('njt-rail') == NjtRailGtfsRealtimeTranslator 24 | assert TranslatorRegistry.get('njt-bus') == NjtBusGtfsRealtimeTranslator 25 | assert TranslatorRegistry.get('path-old') == PathGtfsRealtimeTranslator 26 | assert TranslatorRegistry.get('path-new') == PathNewGtfsRealtimeTranslator 27 | assert TranslatorRegistry.get('swiftly') == SwiftlyGtfsRealtimeTranslator 28 | assert TranslatorRegistry.get('wcdot-bus') == WcdotGtfsRealTimeTranslator 29 | assert TranslatorRegistry.get('mbta') == MbtaGtfsRealtimeTranslator 30 | 31 | def test_registry_for_invalid_key(): 32 | with pytest.warns(TranslatorKeyWarning): 33 | TranslatorRegistry.get('unknown-translator') 34 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/registry/registry.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from gtfs_realtime_translators.translators import LaMetroGtfsRealtimeTranslator, \ 4 | SeptaRegionalRailTranslator, MtaSubwayGtfsRealtimeTranslator, NjtRailGtfsRealtimeTranslator, \ 5 | CtaSubwayGtfsRealtimeTranslator, CtaBusGtfsRealtimeTranslator, PathGtfsRealtimeTranslator, \ 6 | PathNewGtfsRealtimeTranslator, SwiftlyGtfsRealtimeTranslator, WcdotGtfsRealTimeTranslator, \ 7 | NjtBusGtfsRealtimeTranslator, MbtaGtfsRealtimeTranslator, MnmtGtfsRealtimeTranslator, \ 8 | MartaRailGtfsRealtimeTranslator, NjtRailJsonGtfsRealtimeTranslator, \ 9 | NjtBusJsonGtfsRealtimeTranslator, PathRailNewGtfsRealtimeTranslator 10 | 11 | 12 | class TranslatorKeyWarning(Warning): 13 | pass 14 | 15 | 16 | class TranslatorRegistry: 17 | TRANSLATORS = { 18 | 'la-metro-old': LaMetroGtfsRealtimeTranslator, 19 | 'septa-regional-rail': SeptaRegionalRailTranslator, 20 | 'cta-subway': CtaSubwayGtfsRealtimeTranslator, 21 | 'cta-bus': CtaBusGtfsRealtimeTranslator, 22 | 'mta-subway': MtaSubwayGtfsRealtimeTranslator, 23 | 'njt-rail': NjtRailGtfsRealtimeTranslator, 24 | 'njt-rail-json': NjtRailJsonGtfsRealtimeTranslator, 25 | 'njt-bus': NjtBusGtfsRealtimeTranslator, 26 | 'njt-bus-json': NjtBusJsonGtfsRealtimeTranslator, 27 | 'path-old': PathGtfsRealtimeTranslator, 28 | 'path-new': PathNewGtfsRealtimeTranslator, 29 | 'path-rail-new': PathRailNewGtfsRealtimeTranslator, 30 | 'swiftly': SwiftlyGtfsRealtimeTranslator, 31 | 'wcdot-bus': WcdotGtfsRealTimeTranslator, 32 | 'mbta': MbtaGtfsRealtimeTranslator, 33 | 'mnmt': MnmtGtfsRealtimeTranslator, 34 | 'marta-rail': MartaRailGtfsRealtimeTranslator 35 | } 36 | 37 | @classmethod 38 | def get(cls, key): 39 | if key in cls.TRANSLATORS: 40 | return cls.TRANSLATORS[key] 41 | else: 42 | warnings.warn(f'No translator registered for key={key}', TranslatorKeyWarning) 43 | -------------------------------------------------------------------------------- /test/test_cta_bus.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import CtaBusGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 6 | from gtfs_realtime_translators.factories import FeedMessage 7 | 8 | 9 | @pytest.fixture 10 | def cta_bus(): 11 | with open('test/fixtures/cta_bus.json') as f: 12 | raw = f.read() 13 | return raw 14 | 15 | 16 | def test_cta_bus_realtime_arrival(cta_bus): 17 | translator = CtaBusGtfsRealtimeTranslator() 18 | with pendulum.test(pendulum.datetime(2019, 2, 20, 17)): 19 | message = translator(cta_bus) 20 | 21 | entity = message.entity[0] 22 | trip_update = entity.trip_update 23 | stop_time_update = trip_update.stop_time_update[0] 24 | 25 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 26 | 27 | assert entity.id == '1' 28 | assert entity.trip_update.trip.route_id == '147' 29 | assert stop_time_update.stop_id == '1203' 30 | assert stop_time_update.arrival.time == 1570531080 31 | 32 | # Test Intersection extensions 33 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 34 | assert intersection_trip_update.headsign == 'Howard Station' 35 | assert intersection_trip_update.custom_status == '3 min' 36 | 37 | def test_cta_bus_scheduled_departure(cta_bus): 38 | translator = CtaBusGtfsRealtimeTranslator() 39 | with pendulum.test(pendulum.datetime(2019, 2, 20, 17)): 40 | message = translator(cta_bus) 41 | 42 | entity = message.entity[1] 43 | trip_update = entity.trip_update 44 | stop_time_update = trip_update.stop_time_update[0] 45 | 46 | assert entity.id == '2' 47 | assert stop_time_update.arrival.time == 1570531500 48 | 49 | # Test Intersection extensions 50 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 51 | assert intersection_trip_update.custom_status == '10 min' 52 | 53 | -------------------------------------------------------------------------------- /test/fixtures/septa_trolley_lines.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "agencyKey": "septa", 4 | "predictionsData": [ 5 | { 6 | "destinations": [ 7 | { 8 | "directionId": "0", 9 | "headsign": "13th-Market", 10 | "predictions": [ 11 | { 12 | "min": 31, 13 | "scheduleBased": true, 14 | "sec": 1882, 15 | "time": 1623837186, 16 | "tripId": "609813", 17 | "vehicleId": "block_8104_schedBasedVehicle" 18 | } 19 | ] 20 | } 21 | ], 22 | "routeId": "11", 23 | "routeName": "11 - 13th-Market to Darby Trans Cntr", 24 | "routeShortName": "11", 25 | "stopCode": 20646, 26 | "stopId": "20646", 27 | "stopName": "19th St Trolley Station" 28 | }, 29 | { 30 | "destinations": [ 31 | { 32 | "directionId": "0", 33 | "headsign": "13th-Market", 34 | "predictions": [ 35 | { 36 | "min": 11, 37 | "sec": 669, 38 | "time": 1623835973, 39 | "tripId": "610887", 40 | "vehicleId": "9039" 41 | }, 42 | { 43 | "min": 24, 44 | "sec": 1458, 45 | "time": 1623836763, 46 | "tripId": "610888", 47 | "vehicleId": "9048" 48 | }, 49 | { 50 | "min": 39, 51 | "sec": 2395, 52 | "time": 1623837699, 53 | "tripId": "610889", 54 | "vehicleId": "9068" 55 | } 56 | ] 57 | } 58 | ], 59 | "routeId": "13", 60 | "routeName": "13 - 13th-Market to Yeadon-Darby", 61 | "routeShortName": "13", 62 | "stopCode": 20646, 63 | "stopId": "20646", 64 | "stopName": "19th St Trolley Station" 65 | } 66 | ] 67 | }, 68 | "route": "/real-time/septa/predictions GET", 69 | "success": true 70 | } 71 | -------------------------------------------------------------------------------- /test/test_cta_subway.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import CtaSubwayGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 6 | from gtfs_realtime_translators.factories import FeedMessage 7 | 8 | 9 | @pytest.fixture 10 | def cta_subway(): 11 | with open('test/fixtures/cta_subway.json') as f: 12 | raw = f.read() 13 | 14 | return raw 15 | 16 | 17 | def test_cta_subway_realtime_arrival(cta_subway): 18 | translator = CtaSubwayGtfsRealtimeTranslator() 19 | with pendulum.test(pendulum.datetime(2019, 2, 20, 17)): 20 | message = translator(cta_subway) 21 | 22 | entity = message.entity[0] 23 | trip_update = entity.trip_update 24 | stop_time_update = trip_update.stop_time_update[0] 25 | 26 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 27 | 28 | assert entity.id == '1' 29 | assert entity.trip_update.trip.route_id == 'Red' 30 | assert stop_time_update.stop_id == '30251' 31 | assert stop_time_update.arrival.time == 1570458602 32 | 33 | # Test Intersection extensions 34 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 35 | assert intersection_trip_update.headsign == 'Howard' 36 | 37 | intersection_stop_time_update = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 38 | assert intersection_stop_time_update.scheduled_arrival.time == 0 39 | 40 | def test_cta_subway_scheduled_arrival(cta_subway): 41 | translator = CtaSubwayGtfsRealtimeTranslator() 42 | with pendulum.test(pendulum.datetime(2019, 2, 20, 17)): 43 | message = translator(cta_subway) 44 | 45 | entity = message.entity[1] 46 | trip_update = entity.trip_update 47 | stop_time_update = trip_update.stop_time_update[0] 48 | 49 | assert entity.id == '2' 50 | assert stop_time_update.arrival.time == 0 51 | 52 | # Test Intersection extensions 53 | intersection_stop_time_update = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 54 | assert intersection_stop_time_update.scheduled_arrival.time == 1570458776 55 | -------------------------------------------------------------------------------- /test/fixtures/la_metro_rail.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "trip_id": "1234_20190404", 5 | "seconds": 528, 6 | "minutes": 8, 7 | "block_id": "607", 8 | "route_id": "801", 9 | "is_departing": true, 10 | "run_id": "801_1_var0" 11 | }, 12 | { 13 | "trip_id": "1235", 14 | "seconds": 1248, 15 | "minutes": 20.0, 16 | "block_id": "603", 17 | "route_id": "801", 18 | "is_departing": true, 19 | "run_id": "801_1_var0" 20 | }, 21 | { 22 | "trip_id": "1236", 23 | "seconds": 781, 24 | "minutes": 13, 25 | "block_id": "603", 26 | "route_id": "801", 27 | "is_departing": false, 28 | "run_id": "801_0_var0" 29 | }, 30 | { 31 | "trip_id": "1237", 32 | "seconds": 1501, 33 | "minutes": 25, 34 | "block_id": "602", 35 | "route_id": "801", 36 | "is_departing": false, 37 | "run_id": "801_0_var0" 38 | }, 39 | { 40 | "trip_id": "1238", 41 | "seconds": 644, 42 | "minutes": 10, 43 | "block_id": "695", 44 | "route_id": "806", 45 | "is_departing": false, 46 | "run_id": "806_0_var0" 47 | }, 48 | { 49 | "trip_id": "1239", 50 | "seconds": 1322, 51 | "minutes": 22, 52 | "block_id": "610", 53 | "route_id": "806", 54 | "is_departing": false, 55 | "run_id": "806_0_var0" 56 | }, 57 | { 58 | "trip_id": "1240", 59 | "seconds": 348, 60 | "minutes": 5, 61 | "block_id": "694", 62 | "route_id": "806", 63 | "is_departing": true, 64 | "run_id": "806_1_var0" 65 | }, 66 | { 67 | "trip_id": "1241", 68 | "seconds": 1068, 69 | "minutes": 17, 70 | "block_id": "695", 71 | "route_id": "806", 72 | "is_departing": true, 73 | "run_id": "806_1_var0" 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/wcdot_bus.py: -------------------------------------------------------------------------------- 1 | import json 2 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 3 | from gtfs_realtime_translators.validators import RequiredFieldValidator 4 | 5 | 6 | class WcdotGtfsRealTimeTranslator: 7 | TIMEZONE = 'America/New_York' 8 | 9 | def __init__(self, stop_id=None): 10 | RequiredFieldValidator.validate_field_value('stop_id', stop_id) 11 | self.stop_id = stop_id 12 | self.filtered_stops = None 13 | 14 | def __call__(self,data): 15 | json_data = json.loads(data) 16 | entities = json_data["entity"] 17 | trip_updates = self.generate_trip_updates(entities) 18 | return FeedMessage.create(entities=trip_updates) 19 | 20 | def generate_trip_updates(self, entities): 21 | trip_updates = [] 22 | for idx, entity in enumerate(entities): 23 | entity_id = str(idx+1) 24 | trip_update = entity['trip_update'] 25 | trip = trip_update.get('trip') 26 | trip_id = trip.get('trip_id') 27 | route_id = trip.get('route_id') 28 | if route_id: 29 | route_id = route_id.lstrip('0') 30 | stop_time_update = trip_update.get('stop_time_update') 31 | for update in stop_time_update: 32 | stop_id = update.get("stop_id") 33 | arrival = update.get("arrival") 34 | departure = update.get("departure") 35 | if stop_id == self.stop_id: 36 | arrival_delay = None 37 | departure_delay = None 38 | if arrival: 39 | arrival_delay = arrival.get('delay',None) 40 | if departure: 41 | departure_delay = departure.get('delay',None) 42 | trip_update = TripUpdate.create( 43 | entity_id=entity_id, 44 | arrival_delay=arrival_delay, 45 | departure_delay=departure_delay, 46 | trip_id=trip_id, 47 | route_id=route_id, 48 | stop_id=stop_id, 49 | agency_timezone=self.TIMEZONE 50 | ) 51 | trip_updates.append(trip_update) 52 | return trip_updates 53 | -------------------------------------------------------------------------------- /test/test_la_metro.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import LaMetroGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.factories import FeedMessage 6 | 7 | 8 | @pytest.fixture 9 | def la_metro_rail(): 10 | with open('test/fixtures/la_metro_rail.json') as f: 11 | raw = f.read() 12 | 13 | return raw 14 | 15 | 16 | def test_la_data(la_metro_rail): 17 | translator = LaMetroGtfsRealtimeTranslator(stop_id='80122') 18 | with pendulum.test(pendulum.datetime(2019,2,20,17,0,0)): 19 | message = translator(la_metro_rail) 20 | 21 | entity = message.entity[0] 22 | trip_update = entity.trip_update 23 | stop_time_update = trip_update.stop_time_update[0] 24 | 25 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 26 | 27 | assert entity.id == '1' 28 | 29 | assert trip_update.trip.trip_id == '1234' 30 | assert trip_update.trip.route_id == '801' 31 | 32 | assert stop_time_update.arrival.time == 1550682480 33 | assert stop_time_update.departure.time == 1550682480 34 | assert stop_time_update.stop_id == '80122' 35 | 36 | 37 | def test_la_data_with_floats(la_metro_rail): 38 | translator = LaMetroGtfsRealtimeTranslator(stop_id='80122') 39 | with pendulum.test(pendulum.datetime(2019,2,20,17,0,0)): 40 | message = translator(la_metro_rail) 41 | 42 | entity = message.entity[1] 43 | trip_update = entity.trip_update 44 | stop_time_update = trip_update.stop_time_update[0] 45 | 46 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 47 | 48 | assert entity.id == '2' 49 | 50 | assert trip_update.trip.trip_id == '1235' 51 | assert trip_update.trip.route_id == '801' 52 | 53 | assert stop_time_update.arrival.time == 1550683200 54 | assert stop_time_update.departure.time == 1550683200 55 | assert stop_time_update.stop_id == '80122' 56 | 57 | 58 | def test_la_trip_id_parsing(): 59 | assert LaMetroGtfsRealtimeTranslator.calculate_trip_id('48109430_20190404') == '48109430' 60 | assert LaMetroGtfsRealtimeTranslator.calculate_trip_id('48109430_Mo') == '48109430' 61 | assert LaMetroGtfsRealtimeTranslator.calculate_trip_id('48109430') == '48109430' 62 | assert LaMetroGtfsRealtimeTranslator.calculate_trip_id('') == '' 63 | assert LaMetroGtfsRealtimeTranslator.calculate_trip_id(1) == 1 64 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/cta_bus.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pendulum 4 | 5 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 6 | 7 | 8 | class CtaBusGtfsRealtimeTranslator: 9 | TIMEZONE = 'America/Chicago' 10 | 11 | def __call__(self, data): 12 | json_data = json.loads(data) 13 | predictions = json_data.get('bustime-response', {}).get('prd', []) 14 | entities = [self.__make_trip_update(idx, arr) for idx, arr in enumerate(predictions)] 15 | 16 | return FeedMessage.create(entities=entities) 17 | 18 | 19 | @classmethod 20 | def __to_unix_time(cls, time): 21 | return pendulum.parse(time).in_tz(cls.TIMEZONE).int_timestamp 22 | 23 | @classmethod 24 | def __make_trip_update(cls, _id, prediction): 25 | entity_id = str(_id + 1) 26 | route_id = prediction['rt'] 27 | stop_id = prediction['stpid'] 28 | stop_name = prediction['stpnm'] 29 | trip_id = prediction['tatripid'] 30 | 31 | arrival_time = cls.__to_unix_time(prediction['prdtm']) 32 | 33 | ##### Intersection Extensions 34 | headsign = prediction['des'] 35 | custom_status = cls.__get_custom_status(prediction['dyn'], 36 | prediction['dly'], 37 | prediction['prdctdn']) 38 | 39 | return TripUpdate.create(entity_id=entity_id, 40 | route_id=route_id, 41 | stop_id=stop_id, 42 | stop_name=stop_name, 43 | trip_id=trip_id, 44 | arrival_time=arrival_time, 45 | headsign=headsign, 46 | custom_status=custom_status, 47 | agency_timezone=cls.TIMEZONE) 48 | 49 | @classmethod 50 | def __get_custom_status(cls, dynamic_action_type, delay, prediction_time): 51 | if dynamic_action_type == 1: 52 | return 'CANCELED' 53 | if dynamic_action_type == 4: 54 | return 'EXPRESSED' 55 | 56 | if delay: 57 | return 'DELAYED' 58 | 59 | if not prediction_time: 60 | return None 61 | elif prediction_time == 'DUE': 62 | return prediction_time 63 | return f'{prediction_time} min' 64 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/mta_subway.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 4 | 5 | 6 | class MtaSubwayGtfsRealtimeTranslator: 7 | TIMEZONE = 'America/New_York' 8 | 9 | def __call__(self, data): 10 | json_data = json.loads(data) 11 | entities = [] 12 | for stop in json_data: 13 | for group in stop["groups"]: 14 | for idx, arrival in enumerate(group["times"]): 15 | route_id = self.parse_id(group['route']['id']) 16 | stop_name = stop['stop']['name'] 17 | entities.append(self.__make_trip_update(idx, route_id, stop_name, arrival)) 18 | 19 | return FeedMessage.create(entities=entities) 20 | 21 | @classmethod 22 | def parse_id(cls, value): 23 | """ 24 | Some values from the MTA Subway feed come in the form MTASBWY:. 25 | We must parse the id only. 26 | """ 27 | try: 28 | return value.split(':')[1] 29 | except Exception: 30 | return value 31 | 32 | @classmethod 33 | def __make_trip_update(cls, _id, route_id, stop_name, arrival): 34 | entity_id = str(_id + 1) 35 | arrival_time = arrival['serviceDay'] + arrival['realtimeArrival'] 36 | departure_time = arrival['serviceDay'] + arrival['realtimeDeparture'] 37 | trip_id = cls.parse_id(arrival['tripId']) 38 | stop_id = cls.parse_id(arrival['stopId']) 39 | 40 | ##### Intersection Extensions 41 | headsign = arrival['tripHeadsign'] 42 | scheduled_arrival_time = arrival['serviceDay'] + arrival['scheduledArrival'] 43 | scheduled_departure_time = arrival['serviceDay'] + arrival['scheduledDeparture'] 44 | track = arrival.get('track', '') 45 | 46 | return TripUpdate.create(entity_id=entity_id, 47 | arrival_time=arrival_time, 48 | departure_time=departure_time, 49 | trip_id=trip_id, 50 | route_id=route_id, 51 | stop_id=stop_id, 52 | stop_name=stop_name, 53 | headsign=headsign, 54 | scheduled_arrival_time=scheduled_arrival_time, 55 | scheduled_departure_time=scheduled_departure_time, 56 | track=track, 57 | agency_timezone=cls.TIMEZONE) 58 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/bindings/intersection_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: intersection.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf.internal import builder as _builder 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.transit import gtfs_realtime_pb2 as gtfs__realtime__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12intersection.proto\x1a\x13gtfs-realtime.proto\"\xfe\x01\n\x16IntersectionTripUpdate\x12\x10\n\x08headsign\x18\x01 \x01(\t\x12\x18\n\x10route_short_name\x18\x02 \x01(\t\x12\x17\n\x0froute_long_name\x18\x03 \x01(\t\x12\x13\n\x0broute_color\x18\x04 \x01(\t\x12\x18\n\x10route_text_color\x18\x05 \x01(\t\x12\x10\n\x08\x62lock_id\x18\x06 \x01(\t\x12\x17\n\x0f\x61gency_timezone\x18\x07 \x01(\t\x12\x15\n\rcustom_status\x18\x08 \x01(\t\x12\x1a\n\x12scheduled_interval\x18\t \x01(\x05\x12\x12\n\nroute_icon\x18\n \x01(\t\"\xce\x01\n\x1aIntersectionStopTimeUpdate\x12\r\n\x05track\x18\x01 \x01(\t\x12\x45\n\x11scheduled_arrival\x18\x02 \x01(\x0b\x32*.transit_realtime.TripUpdate.StopTimeEvent\x12G\n\x13scheduled_departure\x18\x03 \x01(\x0b\x32*.transit_realtime.TripUpdate.StopTimeEvent\x12\x11\n\tstop_name\x18\x04 \x01(\t\"3\n\x1dIntersectionVehicleDescriptor\x12\x12\n\nrun_number\x18\x01 \x01(\t:X\n\x18intersection_trip_update\x12\x1c.transit_realtime.TripUpdate\x18\xc3\x0f \x01(\x0b\x32\x17.IntersectionTripUpdate:p\n\x1dintersection_stop_time_update\x12+.transit_realtime.TripUpdate.StopTimeUpdate\x18\xc3\x0f \x01(\x0b\x32\x1b.IntersectionStopTimeUpdate:m\n\x1fintersection_vehicle_descriptor\x12#.transit_realtime.VehicleDescriptor\x18\xc3\x0f \x01(\x0b\x32\x1e.IntersectionVehicleDescriptor') 18 | 19 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 20 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'intersection_pb2', globals()) 21 | if _descriptor._USE_C_DESCRIPTORS == False: 22 | gtfs__realtime__pb2.TripUpdate.RegisterExtension(intersection_trip_update) 23 | gtfs__realtime__pb2.TripUpdate.StopTimeUpdate.RegisterExtension(intersection_stop_time_update) 24 | gtfs__realtime__pb2.VehicleDescriptor.RegisterExtension(intersection_vehicle_descriptor) 25 | 26 | DESCRIPTOR._options = None 27 | _INTERSECTIONTRIPUPDATE._serialized_start=44 28 | _INTERSECTIONTRIPUPDATE._serialized_end=298 29 | _INTERSECTIONSTOPTIMEUPDATE._serialized_start=301 30 | _INTERSECTIONSTOPTIMEUPDATE._serialized_end=507 31 | _INTERSECTIONVEHICLEDESCRIPTOR._serialized_start=509 32 | _INTERSECTIONVEHICLEDESCRIPTOR._serialized_end=560 33 | # @@protoc_insertion_point(module_scope) 34 | -------------------------------------------------------------------------------- /test/test_septa.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import SwiftlyGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.factories import FeedMessage 6 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 7 | 8 | 9 | @pytest.fixture 10 | def septa_trolley_lines(): 11 | with open('test/fixtures/septa_trolley_lines.json') as f: 12 | raw = f.read() 13 | 14 | return raw 15 | 16 | 17 | def test_septa_trolley_data(septa_trolley_lines): 18 | translator = SwiftlyGtfsRealtimeTranslator(stop_id='20646') 19 | with pendulum.test(pendulum.datetime(2021,6,16,12,0,0)): 20 | message = translator(septa_trolley_lines) 21 | 22 | # check first entity 23 | entity = message.entity[0] 24 | trip_update = entity.trip_update 25 | stop_time_update = trip_update.stop_time_update[0] 26 | 27 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 28 | 29 | assert len(message.entity) == 4 30 | 31 | assert entity.id == '1' 32 | 33 | assert trip_update.trip.trip_id == '609813' 34 | assert trip_update.trip.route_id == '11' 35 | 36 | assert stop_time_update.arrival.time == 1623846660 37 | assert stop_time_update.departure.time == 1623846660 38 | assert stop_time_update.stop_id == '20646' 39 | 40 | # test extensions 41 | ixn_stop_time_updates = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 42 | ixn_trip_updates = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 43 | assert ixn_stop_time_updates.stop_name == "19th St Trolley Station" 44 | assert ixn_trip_updates.headsign == "13th-Market" 45 | assert ixn_trip_updates.route_short_name == "11" 46 | assert ixn_trip_updates.route_long_name == "11 - 13th-Market to Darby Trans Cntr" 47 | 48 | # check last entity 49 | entity = message.entity[3] 50 | trip_update = entity.trip_update 51 | stop_time_update = trip_update.stop_time_update[0] 52 | 53 | assert entity.id == '3' 54 | 55 | assert trip_update.trip.trip_id == '610889' 56 | assert trip_update.trip.route_id == '13' 57 | 58 | assert stop_time_update.arrival.time == 1623847140 59 | assert stop_time_update.departure.time == 1623847140 60 | assert stop_time_update.stop_id == '20646' 61 | 62 | # test extensions 63 | ixn_stop_time_updates = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 64 | ixn_trip_updates = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 65 | assert ixn_stop_time_updates.stop_name == "19th St Trolley Station" 66 | assert ixn_trip_updates.headsign == "13th-Market" 67 | assert ixn_trip_updates.route_short_name == "13" 68 | assert ixn_trip_updates.route_long_name == "13 - 13th-Market to Yeadon-Darby" 69 | -------------------------------------------------------------------------------- /test/fixtures/njt_bus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16941280 6 | 22899 7 | 2916 8 | 20883 9 | 165 10 | 3 11 | 30-JUN-21 12 | 64J 13 | Weehawken Via Journal Sq 14 | 133 15 | 139HW058 16 | In 17 | 18 | 01-JUL-21 19 | 8:39 AM 20 | -480 21 | 22 | C-1 23 | Journal Square Transportation Center 24 | JERSEY CITY-PATH 25 | 20883 26 | 40.732059 27 | -74.062111 28 | 59 29 | 08:39:00 30 | 31 | 32 | 01-JUL-21 33 | 9:03 AM 34 | -480 35 | 36 | .. 37 | Lincoln Harbor 38 | WEEHAWKEN 39 | 21831 40 | 40.759878 41 | -74.022280 42 | 60 43 | 09:03:00 44 | 45 | 46 | 47 | 17109951 48 | 11276 49 | 2916 50 | 20883 51 | 72 52 | 3 53 | 30-JUN-21 54 | 1 55 | Jersey City Journal Sq Via River Terminal 56 | 3 57 | 001HL008 58 | In 59 | 60 | 01-JUL-21 61 | 8:40 AM 62 | -120 63 | 64 | .. 65 | Journal Square Transportation Center 66 | JERSEY CITY-PATH 67 | 20883 68 | 40.732059 69 | -74.062111 70 | 55 71 | 08:40:00 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /test/test_trip_update.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | 3 | from google.transit import gtfs_realtime_pb2 as gtfs_realtime 4 | 5 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 6 | 7 | 8 | def test_models_schema_output(): 9 | entity_id = '1' 10 | arrival_time = 1234 11 | trip_id = '1234' 12 | stop_id = '2345' 13 | route_id = '3456' 14 | 15 | trip_update = TripUpdate.create(entity_id=entity_id, 16 | arrival_time=arrival_time, 17 | trip_id=trip_id, 18 | stop_id=stop_id, 19 | route_id=route_id) 20 | 21 | entities = [ trip_update ] 22 | message = FeedMessage.create(entities=entities) 23 | 24 | assert type(message) == gtfs_realtime.FeedMessage 25 | assert type(message.header) == gtfs_realtime.FeedHeader 26 | assert isinstance(message.entity, Iterable) 27 | assert len(message.entity) == 1 28 | 29 | entity = message.entity[0] 30 | assert type(entity) == gtfs_realtime.FeedEntity 31 | 32 | trip_update = entity.trip_update 33 | assert type(trip_update) == gtfs_realtime.TripUpdate 34 | assert isinstance(trip_update.stop_time_update, Iterable) 35 | assert len(trip_update.stop_time_update) == 1 36 | assert isinstance(trip_update.trip, gtfs_realtime.TripDescriptor) 37 | 38 | stop_time_update = trip_update.stop_time_update[0] 39 | assert type(stop_time_update) == gtfs_realtime.TripUpdate.StopTimeUpdate 40 | assert type(stop_time_update.arrival) == gtfs_realtime.TripUpdate.StopTimeEvent 41 | assert type(stop_time_update.departure) == gtfs_realtime.TripUpdate.StopTimeEvent 42 | 43 | def test_departure_time_is_used_if_available(): 44 | entity_id = '1' 45 | arrival_time = 1234 46 | departure_time = 2345 47 | trip_id = '1234' 48 | stop_id = '2345' 49 | route_id = '3456' 50 | 51 | entity = TripUpdate.create(entity_id=entity_id, 52 | arrival_time=arrival_time, 53 | departure_time=departure_time, 54 | trip_id=trip_id, 55 | stop_id=stop_id, 56 | route_id=route_id) 57 | 58 | assert entity.trip_update.stop_time_update[0].arrival.time == arrival_time 59 | assert entity.trip_update.stop_time_update[0].departure.time == departure_time 60 | 61 | def test_arrival_time_is_used_if_no_departure_time(): 62 | entity_id = '1' 63 | arrival_time = 1234 64 | trip_id = '1234' 65 | stop_id = '2345' 66 | route_id = '3456' 67 | 68 | entity = TripUpdate.create(entity_id=entity_id, 69 | arrival_time=arrival_time, 70 | trip_id=trip_id, 71 | stop_id=stop_id, 72 | route_id=route_id) 73 | 74 | assert entity.trip_update.stop_time_update[0].arrival.time == arrival_time 75 | assert entity.trip_update.stop_time_update[0].departure.time == arrival_time 76 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/njt_bus_json.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | import re 3 | 4 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 5 | import json 6 | 7 | 8 | class NjtBusJsonGtfsRealtimeTranslator: 9 | 10 | TIMEZONE = 'America/New_York' 11 | 12 | DEFAULT_ROUTE_COLOR = '6C6D71' 13 | DEFAULT_ROUTE_TEXT_COLOR = 'FFFFFF' 14 | 15 | def __init__(self, **kwargs): 16 | self.stop_id = kwargs.get('stop_id') 17 | 18 | def __call__(self, data): 19 | data = json.loads(data) 20 | items = data.get('DVTrip', []) 21 | 22 | entities = self.__make_trip_updates(items, self.stop_id) 23 | return FeedMessage.create(entities=entities) 24 | 25 | @classmethod 26 | def __make_trip_updates(cls, items, stop_id): 27 | trip_updates = [] 28 | 29 | for index, item in enumerate(items): 30 | route_short_name = item.get('public_route', '') 31 | headsign = re.sub(r'^\d+\s*', '', item.get('header', '')) 32 | track = item.get('lanegate', '') 33 | 34 | departure_time_str = item.get('departuretime', '') 35 | scheduled_departure_str = item.get('sched_dep_time', '') 36 | 37 | departure_time = cls.__time_to_unix_time(departure_time_str) 38 | scheduled_departure_time = cls.__time_to_unix_time(scheduled_departure_str) 39 | 40 | arrival_time = departure_time 41 | scheduled_arrival_time = scheduled_departure_time 42 | 43 | trip_update = TripUpdate.create( 44 | entity_id=str(index + 1), 45 | stop_id=stop_id, 46 | departure_time=departure_time, 47 | scheduled_departure_time=scheduled_departure_time, 48 | arrival_time=arrival_time, 49 | scheduled_arrival_time=scheduled_arrival_time, 50 | route_short_name=route_short_name, 51 | route_color=cls.DEFAULT_ROUTE_COLOR, 52 | route_text_color=cls.DEFAULT_ROUTE_TEXT_COLOR, 53 | headsign=headsign, 54 | track=track, 55 | agency_timezone=cls.TIMEZONE, 56 | ) 57 | 58 | trip_updates.append(trip_update) 59 | 60 | return trip_updates 61 | 62 | @classmethod 63 | def __time_to_unix_time(cls, time_str): 64 | """ 65 | Tries to parse a datetime string as either: 66 | - 'M/D/YYYY h:mm:ss A' 67 | - 'h:mm A' (uses today's date) 68 | 69 | Returns: Unix timestamp (int) 70 | """ 71 | time_str = time_str.strip() 72 | 73 | try: 74 | dt = pendulum.from_format(time_str, "M/D/YYYY h:mm:ss A", 75 | tz=cls.TIMEZONE) 76 | except Exception as e: 77 | today = pendulum.today(tz=cls.TIMEZONE).to_date_string() 78 | dt = pendulum.from_format(f"{today} {time_str}", 79 | "YYYY-MM-DD h:mm A", 80 | tz=cls.TIMEZONE) 81 | 82 | return dt.int_timestamp 83 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/mnmt.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pendulum 4 | 5 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 6 | 7 | 8 | class MnmtGtfsRealtimeTranslator: 9 | TIMEZONE = 'America/Chicago' 10 | 11 | def __call__(self, data): 12 | json_data = json.loads(data) 13 | 14 | stops_list = json_data.get('stops') 15 | departures_list = json_data.get('departures') 16 | 17 | entities = [] 18 | if stops_list and departures_list: 19 | entities = self.__make_trip_updates(stops_list, departures_list) 20 | 21 | return FeedMessage.create(entities=entities) 22 | 23 | @classmethod 24 | def __make_trip_updates(cls, stops_list, departures_list): 25 | trip_updates = [] 26 | stop_name = stops_list[0].get("description") 27 | 28 | for index, departure in enumerate(departures_list): 29 | entity_id = str(index + 1) 30 | 31 | trip_id = departure.get('trip_id') 32 | 33 | stop_id = departure.get('stop_id') 34 | if stop_id: 35 | stop_id = str(stop_id) 36 | 37 | headsign = departure.get('description') 38 | route_id = departure.get('route_id') 39 | direction_id = departure.get('direction_id') 40 | 41 | departure_time, scheduled_departure_time = None, None 42 | arrival_time, scheduled_arrival_time = None, None 43 | if cls.__is_realtime_departure(departure): 44 | departure_time = departure.get('departure_time') 45 | arrival_time = departure_time 46 | else: 47 | scheduled_departure_time = departure.get('departure_time') 48 | scheduled_arrival_time = scheduled_departure_time 49 | 50 | route_short_name = cls.__get_route_short_name(departure) 51 | 52 | trip_update = TripUpdate.create(entity_id=entity_id, 53 | departure_time=departure_time, 54 | arrival_time=arrival_time, 55 | scheduled_departure_time=scheduled_departure_time, 56 | scheduled_arrival_time=scheduled_arrival_time, 57 | trip_id=trip_id, 58 | route_id=route_id, 59 | route_short_name=route_short_name, 60 | stop_id=stop_id, 61 | stop_name=stop_name, 62 | headsign=headsign, 63 | direction_id=direction_id, 64 | agency_timezone=cls.TIMEZONE 65 | ) 66 | 67 | trip_updates.append(trip_update) 68 | 69 | return trip_updates 70 | 71 | @classmethod 72 | def __is_realtime_departure(cls, departure): 73 | return departure.get('actual') is True 74 | 75 | @classmethod 76 | def __get_route_short_name(cls, departure): 77 | terminal = departure.get('terminal') 78 | route_short_name = departure.get('route_short_name') 79 | if terminal: 80 | return f'{route_short_name}{terminal}' 81 | return route_short_name 82 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/swiftly.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | 4 | import pendulum 5 | 6 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 7 | from gtfs_realtime_translators.validators import RequiredFieldValidator 8 | 9 | 10 | class SwiftlyGtfsRealtimeTranslator: 11 | AGENCY_TIMEZONE_MAP = { 12 | 'lametro': 'America/Los_Angeles', 13 | 'lametro-rail': 'America/Los_Angeles', 14 | 'septa': 'America/New_York', 15 | } 16 | 17 | def __init__(self, stop_id=None): 18 | RequiredFieldValidator.validate_field_value('stop_id', stop_id) 19 | self.stop_id = stop_id 20 | 21 | def __call__(self, feed): 22 | json_feed = json.loads(feed) 23 | json_data = json_feed.get("data", {}) 24 | entities = [] 25 | agency_key = json_data.get("agencyKey", None) 26 | timezone = self.__get_timezone(agency_key) 27 | for data in json_data.get("predictionsData", []): 28 | stop_id = data.get("stopId", None) 29 | RequiredFieldValidator.validate_field_value('stop_id', stop_id) 30 | trip_updates = self.__make_trip_updates(data, stop_id, timezone) 31 | entities.extend(trip_updates) 32 | 33 | return FeedMessage.create(entities=entities) 34 | 35 | @classmethod 36 | def __make_trip_updates(cls, data, stop_id, timezone): 37 | trip_updates = [] 38 | route_id = data.get("routeId") 39 | 40 | # Intersection Extensions 41 | route_short_name = data.get("routeShortName") 42 | route_long_name = data.get("routeName") 43 | stop_name = data.get("stopName") 44 | 45 | for destination in data["destinations"]: 46 | # Intersection Extension 47 | headsign = destination.get("headsign") 48 | direction_id = int(destination.get("directionId", -1)) 49 | # realtime predictions 50 | predictions = enumerate(destination["predictions"]) 51 | for _idx, arrival in predictions: 52 | entity_id = str(_idx + 1) 53 | now = int(pendulum.now().timestamp()) 54 | arrival_or_departure_time = now + \ 55 | math.floor(arrival.get("sec") / 60) * 60 56 | trip_id = arrival.get('tripId') 57 | 58 | trip_update = TripUpdate.create(entity_id=entity_id, 59 | arrival_time=arrival_or_departure_time, 60 | departure_time=arrival_or_departure_time, 61 | trip_id=trip_id, 62 | route_id=route_id, 63 | route_short_name=route_short_name, 64 | route_long_name=route_long_name, 65 | stop_id=stop_id, 66 | stop_name=stop_name, 67 | headsign=headsign, 68 | direction_id=direction_id, 69 | agency_timezone=timezone 70 | ) 71 | 72 | trip_updates.append(trip_update) 73 | 74 | return trip_updates 75 | 76 | @classmethod 77 | def __get_timezone(cls, agency_key): 78 | if not agency_key: 79 | return None 80 | return cls.AGENCY_TIMEZONE_MAP.get(agency_key, None) 81 | -------------------------------------------------------------------------------- /test/test_mnmt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gtfs_realtime_translators.translators import MnmtGtfsRealtimeTranslator 4 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 5 | from gtfs_realtime_translators.factories import FeedMessage 6 | 7 | 8 | @pytest.fixture 9 | def mnmt(): 10 | with open('test/fixtures/mnmt.json') as f: 11 | raw = f.read() 12 | return raw 13 | 14 | 15 | def test_mnmt_realtime_departure(mnmt): 16 | translator = MnmtGtfsRealtimeTranslator() 17 | message = translator(mnmt) 18 | 19 | entity = message.entity[0] 20 | trip_update = entity.trip_update 21 | stop_time_update = trip_update.stop_time_update[0] 22 | 23 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 24 | assert entity.id == '1' 25 | 26 | assert stop_time_update.departure.time == 1702923655 27 | assert stop_time_update.arrival.time == 1702923655 28 | 29 | assert not stop_time_update.Extensions[ 30 | intersection_gtfs_realtime.intersection_stop_time_update].\ 31 | scheduled_arrival.time 32 | assert not stop_time_update.Extensions[ 33 | intersection_gtfs_realtime.intersection_stop_time_update].\ 34 | scheduled_departure.time 35 | 36 | 37 | def test_mnmt_scheduled_departure(mnmt): 38 | translator = MnmtGtfsRealtimeTranslator() 39 | message = translator(mnmt) 40 | 41 | entity = message.entity[1] 42 | trip_update = entity.trip_update 43 | stop_time_update = trip_update.stop_time_update[0] 44 | 45 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 46 | assert entity.id == '2' 47 | 48 | assert not stop_time_update.departure.time 49 | assert not stop_time_update.arrival.time 50 | 51 | assert stop_time_update.Extensions[ 52 | intersection_gtfs_realtime.intersection_stop_time_update].\ 53 | scheduled_arrival.time == 1702924920 54 | assert stop_time_update.Extensions[ 55 | intersection_gtfs_realtime.intersection_stop_time_update].\ 56 | scheduled_departure.time == 1702924920 57 | 58 | 59 | def test_mnmt_route_short_name_with_terminal(mnmt): 60 | translator = MnmtGtfsRealtimeTranslator() 61 | message = translator(mnmt) 62 | 63 | entity = message.entity[0] 64 | trip_update = entity.trip_update 65 | 66 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 67 | assert entity.id == '1' 68 | 69 | assert trip_update.Extensions[ 70 | intersection_gtfs_realtime.intersection_trip_update].\ 71 | route_short_name == '22A' 72 | 73 | 74 | def test_mnmt_route_short_name_without_terminal(mnmt): 75 | translator = MnmtGtfsRealtimeTranslator() 76 | message = translator(mnmt) 77 | 78 | entity = message.entity[1] 79 | trip_update = entity.trip_update 80 | 81 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 82 | assert entity.id == '2' 83 | 84 | assert trip_update.Extensions[ 85 | intersection_gtfs_realtime.intersection_trip_update].\ 86 | route_short_name == '22' 87 | 88 | 89 | def test_mnmt_headsign(mnmt): 90 | translator = MnmtGtfsRealtimeTranslator() 91 | message = translator(mnmt) 92 | 93 | entity = message.entity[0] 94 | trip_update = entity.trip_update 95 | 96 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 97 | assert entity.id == '1' 98 | 99 | assert trip_update.Extensions[ 100 | intersection_gtfs_realtime.intersection_trip_update].headsign \ 101 | == 'Brklyn Ctr Tc / N Lyndale / Via Penn Av' 102 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/cta_subway.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pendulum 4 | 5 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 6 | 7 | 8 | class CtaSubwayGtfsRealtimeTranslator: 9 | TIMEZONE = 'America/Chicago' 10 | 11 | def __init__(self, **kwargs): 12 | stop_list = kwargs.get('stop_list') 13 | if stop_list: 14 | self.stop_list = stop_list.split(',') 15 | else: 16 | self.stop_list = None 17 | 18 | def __call__(self, data): 19 | json_data = json.loads(data) 20 | predictions = json_data.get('ctatt', {}).get('eta', []) 21 | 22 | entities = [] 23 | for idx, prediction in enumerate(predictions): 24 | stop_id = prediction['stpId'] 25 | if not self.stop_list or stop_id in self.stop_list: 26 | entities.append(self.__make_trip_update(idx, prediction)) 27 | 28 | return FeedMessage.create(entities=entities) 29 | 30 | 31 | @classmethod 32 | def __to_unix_time(cls, time): 33 | return pendulum.parse(time).in_tz(cls.TIMEZONE).int_timestamp 34 | 35 | @classmethod 36 | def __make_trip_update(cls, _id, prediction): 37 | entity_id = str(_id + 1) 38 | route_id = prediction['rt'] 39 | stop_id = prediction['stpId'] 40 | 41 | is_scheduled = prediction['isSch'] == '1' 42 | parsed_arrival_time = cls.__to_unix_time(prediction['arrT']) 43 | arrival_time = None if is_scheduled else parsed_arrival_time 44 | 45 | ##### Intersection Extensions 46 | headsign = prediction['destNm'] 47 | scheduled_arrival_time = parsed_arrival_time if is_scheduled else None 48 | 49 | parsed_prediction_time = cls.__to_unix_time(prediction['prdt']) 50 | is_app = prediction.get('isApp', '0') 51 | custom_status = cls.__get_custom_status(is_app, parsed_arrival_time, 52 | parsed_prediction_time) 53 | scheduled_interval = cls.__get_scheduled_interval(is_scheduled, 54 | prediction['schInt']) 55 | 56 | route_icon = cls.__get_route_icon(prediction['flags'], headsign) 57 | run_number = prediction['rn'] 58 | 59 | return TripUpdate.create(entity_id=entity_id, 60 | route_id=route_id, 61 | stop_id=stop_id, 62 | arrival_time=arrival_time, 63 | headsign=headsign, 64 | scheduled_arrival_time=scheduled_arrival_time, 65 | custom_status=custom_status, 66 | agency_timezone=cls.TIMEZONE, 67 | scheduled_interval=scheduled_interval, 68 | route_icon=route_icon, 69 | run_number=run_number) 70 | 71 | @classmethod 72 | def __get_custom_status(cls, is_app, arrival_time, prediction_time): 73 | is_approaching = is_app == '1' 74 | if is_approaching: 75 | return 'DUE' 76 | 77 | seconds_until_train_arrives = arrival_time - prediction_time 78 | if seconds_until_train_arrives <= 90: 79 | return 'DUE' 80 | 81 | minutes_until_train_arrives = seconds_until_train_arrives / 60 82 | return f'{round(minutes_until_train_arrives)} min' 83 | 84 | @classmethod 85 | def __get_scheduled_interval(cls, is_scheduled, scheduled_interval): 86 | if is_scheduled and scheduled_interval: 87 | scheduled_interval_seconds = int(scheduled_interval) * 60 88 | return scheduled_interval_seconds 89 | return None 90 | 91 | @classmethod 92 | def __get_route_icon(cls, flags, headsign): 93 | if flags and flags.lower() == 'h': 94 | return 'holiday' 95 | if headsign and headsign.lower() in ["midway", "o'hare"]: 96 | return 'airport' 97 | return None 98 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/marta_rail.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pendulum 4 | 5 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 6 | 7 | 8 | class MartaRailGtfsRealtimeTranslator: 9 | TIMEZONE = 'America/New_York' 10 | 11 | LINE_ROUTE_DATA_MAP = { 12 | 'BLUE': ('BLUE', 'Blue Line', '0075B2', 'FFFFFF'), 13 | 'GOLD': ('GOLD', 'Gold Line', 'D4A723', '000000'), 14 | 'GREEN': ('GREEN', 'Green Line', '009D4B', '000000'), 15 | 'RED': ('RED', 'Red Line', 'CE242B', 'FFFFFF'), 16 | } 17 | 18 | LINE_STATION_DESTINATION_STOP_ID_MAP = { 19 | ('GOLD', 'LINDBERGH STATION', 'DORAVILLE'): '58', 20 | ('GOLD', 'LINDBERGH STATION', 'AIRPORT'): '57', 21 | ('RED', 'LINDBERGH STATION', 'NORTH SPRINGS'): '999549', 22 | ('RED', 'LINDBERGH STATION', 'AIRPORT'): '999549', 23 | ('RED', 'LINDBERGH STATION', 'LINDBERGH'): '999690', 24 | } 25 | 26 | def __call__(self, data): 27 | predictions = json.loads(data) 28 | entities = [] 29 | current_date = pendulum.today(self.TIMEZONE).format('YYYY-MM-DD') 30 | for index, prediction in enumerate(predictions): 31 | entities.append(self.__make_trip_update(index, 32 | prediction, 33 | current_date)) 34 | 35 | return FeedMessage.create(entities=entities) 36 | 37 | @classmethod 38 | def __make_trip_update(cls, _id, prediction, current_date): 39 | entity_id = str(_id + 1) 40 | vehicle_id = prediction.get('TRAIN_ID') 41 | stop_name = prediction.get('STATION') 42 | headsign = prediction.get('DESTINATION') 43 | custom_status = prediction.get('WAITING_TIME') 44 | station = prediction.get('STATION') 45 | line = prediction.get('LINE') 46 | 47 | route_data = cls.__get_route_data(line) 48 | route_short_name = route_data[0] 49 | route_long_name = route_data[1] 50 | route_color = route_data[2] 51 | route_text_color = route_data[3] 52 | 53 | stop_id = cls.__get_stop_id(line, station, headsign) 54 | 55 | unix_arrival_time = cls.__to_unix_time(prediction.get('NEXT_ARR'), 56 | current_date) 57 | is_realtime = prediction.get('IS_REALTIME') 58 | 59 | arrival_time = unix_arrival_time if is_realtime else None 60 | scheduled_arrival_time = None if is_realtime else unix_arrival_time 61 | 62 | return TripUpdate.create(entity_id=entity_id, 63 | route_short_name=route_short_name, 64 | route_long_name=route_long_name, 65 | route_color=route_color, 66 | route_text_color=route_text_color, 67 | stop_id=stop_id, 68 | vehicle_id=vehicle_id, 69 | stop_name=stop_name, 70 | headsign=headsign, 71 | custom_status=custom_status, 72 | arrival_time=arrival_time, 73 | scheduled_arrival_time=scheduled_arrival_time, 74 | agency_timezone=cls.TIMEZONE) 75 | 76 | @classmethod 77 | def __get_route_data(cls, line): 78 | return cls.LINE_ROUTE_DATA_MAP.get(line) 79 | 80 | @classmethod 81 | def __get_stop_id(cls, line, station, headsign): 82 | return cls.LINE_STATION_DESTINATION_STOP_ID_MAP.get((line.upper(), 83 | station.upper(), 84 | headsign.upper())) 85 | 86 | @classmethod 87 | def __to_unix_time(cls, time, current_date): 88 | datetime_str = f"{current_date} {time}" 89 | dt = pendulum.from_format(datetime_str, 90 | 'YYYY-MM-DD h:mm:ss A', 91 | tz=cls.TIMEZONE).in_tz('UTC') 92 | return dt.int_timestamp 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gtfs-realtime-translators 2 | 3 | [![Build Status](https://travis-ci.org/Intersection/gtfs-realtime-translators.svg?branch=master)](https://travis-ci.org/Intersection/gtfs-realtime-translators) [![Total alerts](https://img.shields.io/lgtm/alerts/g/Intersection/gtfs-realtime-translators.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Intersection/gtfs-realtime-translators/alerts/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/Intersection/gtfs-realtime-translators.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Intersection/gtfs-realtime-translators/context:python) 4 | 5 | `gtfs-realtime-translators` translates custom arrivals formats to GTFS-realtime. It uses the Google [`gtfs-realtime-bindings`](https://github.com/google/gtfs-realtime-bindings/tree/master/python) for Python, supplemented by Intersection extensions. 6 | 7 | ## Overview 8 | 9 | Following the [GTFS-realtime spec](https://developers.google.com/transit/gtfs-realtime/), its three pillars are: 10 | - `TripUpdate` 11 | - `VehiclePosition` 12 | - `Alert` 13 | 14 | A `FeedMessage` is a list of _entities_, each of which is one of the types above. A `FeedMessage` may have entities of different types. 15 | 16 | As of 2019-06-15, only `TripUpdate` is supported. 17 | 18 | ## Installation 19 | ``` 20 | pip install -e git+https://github.com/Intersection/gtfs-realtime-translators.git@#egg=gtfs-realtime-translators 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Registry 26 | The registry is used to return a translator for a given translator key. This is useful to decouple the translator lookup via external systems from its implementation. 27 | ``` 28 | from gtfs_realtime_translators.registry import TranslatorRegistry 29 | 30 | translator_klass = TranslatorRegistry.get('la-metro') 31 | translator = translator_klass(**kwargs) 32 | ``` 33 | 34 | ### Translators 35 | ``` 36 | from gtfs_realtime_translators.translators import LaMetroGtfsRealtimeTranslator 37 | 38 | translator = LaMetroGtfsRealtimeTranslator(stop_id='80122') 39 | feed_message = translator(la_metro_rail_input_data) 40 | ``` 41 | At this point, `feed_message` is a standard protocol buffer object and adheres to the normal [Python Protocol Buffer interface](https://developers.google.com/protocol-buffers/docs/pythontutorial). 42 | ``` 43 | feed_bytes = feed_message.SerializeToString() 44 | ``` 45 | 46 | ### Factories 47 | New translators should be contributed back to this library. 48 | ``` 49 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 50 | 51 | trip_update = TripUpdate.create(entity_id=entity_id, 52 | arrival_time=arrival_time, 53 | departure_time=departure_time, 54 | trip_id=trip_id, 55 | stop_id=stop_id, 56 | route_id=route_id) 57 | 58 | entities = [ trip_update ] 59 | 60 | feed_message = FeedMessage.create(entities=entities) 61 | ``` 62 | 63 | ## GTFS-Realtime Bindings 64 | 65 | ### Source `gtfs-realtime.proto` 66 | The GTFS-realtime spec is maintained in the [google/transit](https://github.com/google/transit.git) repository. Currently, since there is no versioned way to programmatically include this in our projects, we must clone it as a manual dependency. 67 | ``` 68 | git clone https://github.com/google/transit.git ../google-transit 69 | cp ../google-transit/gtfs-realtime/proto/gtfs-realtime.proto gtfs_realtime_translators/bindings/ 70 | ``` 71 | 72 | ### Re-generate Bindings 73 | For our Python libraries to understand the interface specified by the GTFS-realtime spec, we must generate language bindings. 74 | ``` 75 | virtualenv ~/.env/gtfs-realtime-bindings 76 | source ~/.env/gtfs-realtime-bindings/bin/activate 77 | pip install grpcio-tools 78 | python3 -m grpc_tools.protoc -I gtfs_realtime_translators/bindings/ --python_out=gtfs_realtime_translators/bindings/ gtfs_realtime_translators/bindings/intersection.proto 79 | ``` 80 | Since we are using the published spec bindings, we must do one more step. Inside the generated file, `gtfs_realtime_translators/bindings/intersection_pb2.py`, change the following line 81 | ``` 82 | import gtfs_realtime_pb2 as gtfs__realtime__pb2 83 | ``` 84 | to 85 | ``` 86 | from google.transit import gtfs_realtime_pb2 as gtfs__realtime__pb2 87 | ``` 88 | 89 | ## Run Tests Locally 90 | 91 | ``` 92 | pip install -r requirements.txt 93 | pip install -e . 94 | pytest 95 | ``` 96 | -------------------------------------------------------------------------------- /test/test_septa_regional_rail.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.translators import SeptaRegionalRailTranslator 5 | from gtfs_realtime_translators.factories import FeedMessage 6 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 7 | 8 | 9 | @pytest.fixture 10 | def septa_regional_rail(): 11 | with open('test/fixtures/septa_regional_rail.json') as f: 12 | raw = f.read() 13 | 14 | return raw 15 | 16 | 17 | def test_septa_regional_rail(septa_regional_rail): 18 | with pendulum.test(pendulum.datetime(2019,4,26,15,0,0, tz='America/New_York')): 19 | translator = SeptaRegionalRailTranslator(stop_id='90004', filter_seconds=7200) 20 | message = translator(septa_regional_rail) 21 | 22 | assert len(message.entity) == 57 23 | 24 | entity = message.entity[0] 25 | trip_update = entity.trip_update 26 | stop_time_update = trip_update.stop_time_update[0] 27 | 28 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 29 | 30 | assert entity.id == '1' 31 | 32 | assert trip_update.trip.route_id == 'FOX' 33 | assert trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].headsign == 'Fox Chase' 34 | 35 | assert stop_time_update.arrival.time == 1556305921 36 | assert stop_time_update.departure.time == 1556305981 37 | assert stop_time_update.stop_id == '90004' 38 | assert stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].track == '2' 39 | 40 | 41 | def test_septa_regional_rail_with_delay(septa_regional_rail): 42 | with pendulum.test(pendulum.datetime(2019,4,26,15,0,0, tz='America/New_York')): 43 | translator = SeptaRegionalRailTranslator(stop_id='90004', filter_seconds=7200) 44 | message = translator(septa_regional_rail) 45 | 46 | entity = message.entity[2] 47 | trip_update = entity.trip_update 48 | stop_time_update = trip_update.stop_time_update[0] 49 | 50 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 51 | 52 | assert entity.id == '3' 53 | 54 | assert trip_update.trip.route_id == 'MED' 55 | assert trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].headsign == 'Warminster' 56 | 57 | assert stop_time_update.arrival.time == 1556306641 58 | assert stop_time_update.departure.time == 1556306700 59 | assert stop_time_update.stop_id == '90004' 60 | assert stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].scheduled_arrival.time == 1556306581 61 | assert stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].scheduled_departure.time == 1556306640 62 | assert stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].track == '5' 63 | 64 | 65 | def test_transform_arrival(): 66 | arrival = { 67 | "direction": "N", 68 | "path": "R8N", 69 | "train_id": "838", 70 | "origin": "30th Street Station", 71 | "destination": "Fox Chase", 72 | "line": "Fox Chase", 73 | "status": "On Time", 74 | "service_type": "LOCAL", 75 | "next_station": "30th St", 76 | "sched_time": "2019-04-26 15:12:01.000", 77 | "depart_time": "2019-04-26 15:13:01.000", 78 | "track": "2", 79 | "track_change": None, 80 | "platform": "", 81 | "platform_change": None 82 | } 83 | 84 | transformed = SeptaRegionalRailTranslator.transform_arrival(arrival) 85 | 86 | assert arrival.keys() == transformed.keys() 87 | 88 | assert transformed['sched_time'] == 1556305921 89 | assert transformed['depart_time'] == 1556305981 90 | assert type(transformed['sched_time']) == int 91 | assert type(transformed['depart_time']) == int 92 | 93 | 94 | def test_calculate_realtime(): 95 | assert SeptaRegionalRailTranslator.calculate_realtime(1556305921, '1 min') == 1556305981 96 | assert SeptaRegionalRailTranslator.calculate_realtime(1556305921, '12 min') == 1556306641 97 | assert SeptaRegionalRailTranslator.calculate_realtime(1556305921, 'On Time') == 1556305921 98 | 99 | 100 | def test_time_at(): 101 | with pendulum.test(pendulum.datetime(2019,3,8,12,0,0)): 102 | assert SeptaRegionalRailTranslator.calculate_time_at(seconds=1) == int(pendulum.datetime(2019,3,8,12,0,1).timestamp()) 103 | -------------------------------------------------------------------------------- /test/fixtures/mbta_subway_missing_static.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "attributes": { 5 | "arrival_time": "2023-05-03T16:05:15-04:00", 6 | "departure_time": "2023-05-03T16:06:21-04:00", 7 | "direction_id": 0, 8 | "schedule_relationship": null, 9 | "status": null, 10 | "stop_sequence": 70 11 | }, 12 | "id": "prediction-55458948-19:45-GovernmentCenterWonderlandSuspend-70045-70", 13 | "relationships": { 14 | "route": { 15 | "data": { 16 | "id": "Blue", 17 | "type": "route" 18 | } 19 | }, 20 | "schedule": { 21 | "data": { 22 | "id": "schedule_id_70045", 23 | "type": "schedule" 24 | } 25 | }, 26 | "stop": { 27 | "data": { 28 | "id": "70045", 29 | "type": "stop" 30 | } 31 | }, 32 | "trip": { 33 | "data": { 34 | "id": "55458948-19:45-GovernmentCenterWonderlandSuspend", 35 | "type": "trip" 36 | } 37 | }, 38 | "vehicle": { 39 | "data": { 40 | "id": "B-547674D2", 41 | "type": "vehicle" 42 | } 43 | } 44 | }, 45 | "type": "prediction" 46 | } 47 | ], 48 | "included": [ 49 | { 50 | "attributes": { 51 | "bikes_allowed": 0, 52 | "block_id": "B946_-2", 53 | "direction_id": 0, 54 | "headsign": "Bowdoin", 55 | "name": "", 56 | "wheelchair_accessible": 1 57 | }, 58 | "id": "55458948-19:45-GovernmentCenterWonderlandSuspend", 59 | "links": { 60 | "self": "/trips/55458948-19%3A45-GovernmentCenterWonderlandSuspend" 61 | }, 62 | "relationships": { 63 | "route": { 64 | "data": { 65 | "id": "Blue", 66 | "type": "route" 67 | } 68 | }, 69 | "route_pattern": { 70 | "data": { 71 | "id": "Blue-6-0", 72 | "type": "route_pattern" 73 | } 74 | }, 75 | "service": { 76 | "data": { 77 | "id": "RTL223-1-Wdy-01-GvtCnrWndSsd", 78 | "type": "service" 79 | } 80 | }, 81 | "shape": { 82 | "data": { 83 | "id": "946_0013", 84 | "type": "shape" 85 | } 86 | } 87 | }, 88 | "type": "trip" 89 | }, 90 | { 91 | "attributes": { 92 | "color": "003DA5", 93 | "description": "Rapid Transit", 94 | "direction_destinations": [ 95 | "Bowdoin", 96 | "Wonderland" 97 | ], 98 | "direction_names": [ 99 | "West", 100 | "East" 101 | ], 102 | "fare_class": "Rapid Transit", 103 | "long_name": "Blue Line", 104 | "short_name": "", 105 | "sort_order": 10040, 106 | "text_color": "FFFFFF", 107 | "type": 1 108 | }, 109 | "id": "Blue", 110 | "links": { 111 | "self": "/routes/Blue" 112 | }, 113 | "relationships": { 114 | "line": { 115 | "data": { 116 | "id": "line-Blue", 117 | "type": "line" 118 | } 119 | } 120 | }, 121 | "type": "route" 122 | } 123 | ], 124 | "jsonapi": { 125 | "version": "1.0" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test/fixtures/path_rail.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "consideredStation": "33S", 5 | "label": "ToNJ", 6 | "target": "JSQ", 7 | "messages": [ 8 | { 9 | "secondsToArrival": 132, 10 | "arrivalTimeMessage": "2 min", 11 | "lineColor": "#FF9900", 12 | "headSign": "Journal Square", 13 | "lastUpdated": "2020-02-21T15:04:47.529288-05:00" 14 | }, 15 | { 16 | "secondsToArrival": 732, 17 | "arrivalTimeMessage": "12 min", 18 | "lineColor": "#FF9900", 19 | "headSign": "Journal Square", 20 | "lastUpdated": "2020-02-21T15:04:47.529288-05:00" 21 | } 22 | ] 23 | }, 24 | { 25 | "consideredStation": "HOB", 26 | "label": "ToNY", 27 | "target": "WTC", 28 | "messages": [ 29 | { 30 | "secondsToArrival": 248, 31 | "arrivalTimeMessage": "4 min", 32 | "lineColor": "#65C100", 33 | "headSign": "World Trade Center", 34 | "lastUpdated": "2020-02-21T15:04:52.397063-05:00" 35 | }, 36 | { 37 | "secondsToArrival": 968, 38 | "arrivalTimeMessage": "16 min", 39 | "lineColor": "#65C100", 40 | "headSign": "World Trade Center", 41 | "lastUpdated": "2020-02-21T15:04:52.397063-05:00" 42 | } 43 | ] 44 | }, 45 | { 46 | "consideredStation": "GRV", 47 | "label": "ToNJ", 48 | "target": "JSQ", 49 | "messages": [ 50 | { 51 | "secondsToArrival": 167, 52 | "arrivalTimeMessage": "3 min", 53 | "lineColor": "#4D92FB,#FF9900", 54 | "headSign": "Journal Square via Hoboken", 55 | "lastUpdated": "2020-02-22T15:45:59.562598-05:00" 56 | } 57 | ] 58 | }, 59 | { 60 | "consideredStation": "GRV", 61 | "label": "ToNY", 62 | "target": "EXP", 63 | "messages": [ 64 | { 65 | "secondsToArrival": 123, 66 | "arrivalTimeMessage": "2 min", 67 | "lineColor": "#D93A30", 68 | "headSign": "Exchange Place", 69 | "lastUpdated": "2020-02-22T15:46:24.456122-05:00" 70 | } 71 | ] 72 | }, 73 | { 74 | "consideredStation": "HAR", 75 | "label": "ToNY", 76 | "target": "EXP", 77 | "messages": [ 78 | { 79 | "secondsToArrival": 903, 80 | "arrivalTimeMessage": "15 min", 81 | "lineColor": "#D93A30", 82 | "headSign": "Exchange Place", 83 | "lastUpdated": "2020-02-22T15:46:24.456122-05:00" 84 | }, 85 | { 86 | "secondsToArrival": 2103, 87 | "arrivalTimeMessage": "35 min", 88 | "lineColor": "#D93A30", 89 | "headSign": "Exchange Place", 90 | "lastUpdated": "2020-02-22T15:46:24.456122-05:00" 91 | } 92 | ] 93 | }, 94 | { 95 | "consideredStation": "NEW", 96 | "label": "ToNJ", 97 | "target": "JSQ", 98 | "messages": [ 99 | { 100 | "secondsToArrival": 379, 101 | "arrivalTimeMessage": "6 min", 102 | "lineColor": "#4D92FB,#FF9900", 103 | "headSign": "Journal Square via Hoboken", 104 | "lastUpdated": "2020-02-22T15:46:19.622791-05:00" 105 | }, 106 | { 107 | "secondsToArrival": 988, 108 | "arrivalTimeMessage": "17 min", 109 | "lineColor": "#4D92FB,#FF9900", 110 | "headSign": "Journal Square via Hoboken", 111 | "lastUpdated": "2020-02-22T15:46:19.622791-05:00" 112 | } 113 | ] 114 | }, 115 | { 116 | "consideredStation": "CHR", 117 | "label": "ToNJ", 118 | "target": "JSQ", 119 | "messages": [ 120 | { 121 | "secondsToArrival": 124, 122 | "arrivalTimeMessage": "Delayed", 123 | "lineColor": "#4D92FB,#FF9900", 124 | "headSign": "Journal Square via Hoboken", 125 | "lastUpdated": "2020-02-22T15:46:19.622791-05:00" 126 | }, 127 | { 128 | "secondsToArrival": 515, 129 | "arrivalTimeMessage": "9 min", 130 | "lineColor": "#4D92FB,#FF9900", 131 | "headSign": "Journal Square via Hoboken", 132 | "lastUpdated": "2020-02-22T15:46:19.622791-05:00" 133 | } 134 | ] 135 | }, 136 | { 137 | "consideredStation": "09S", 138 | "label": "ToNJ", 139 | "target": "HOB", 140 | "messages": [ 141 | { 142 | "secondsToArrival": 88, 143 | "arrivalTimeMessage": "2 min", 144 | "lineColor": "#4D92FB", 145 | "headSign": "Hoboken", 146 | "lastUpdated": "2020-02-21T15:04:47.529288-05:00" 147 | } 148 | ] 149 | } 150 | ], 151 | "requestTimestamp": "2020-02-22T20:46:26" 152 | } -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/septa_regional_rail.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import copy 4 | 5 | import pendulum 6 | 7 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 8 | from gtfs_realtime_translators.validators import RequiredFieldValidator 9 | 10 | 11 | class SeptaRegionalRailTranslator: 12 | TIMEZONE = 'America/New_York' 13 | STATUS_PATTERN = re.compile(r'(?P\d*) min') 14 | ROUTE_ID_LOOKUP = { 15 | 'Airport': 'AIR', 16 | 'Chestnut Hill East': 'CHE', 17 | 'Chestnut Hill West': 'CHW', 18 | 'Lansdale/Doylestown': 'LAN', 19 | 'Media/Wawa': 'MED', 20 | 'Fox Chase': 'FOX', 21 | 'Manayunk/Norristown': 'NOR', 22 | 'Paoli/Thorndale': 'PAO', 23 | 'Cynwyd': 'CYN', 24 | 'Trenton': 'TRE', 25 | 'Warminster': 'WAR', 26 | 'Wilmington/Newark': 'WIL', 27 | 'West Trenton': 'WTR', 28 | } 29 | 30 | def __init__(self, **kwargs): 31 | self.stop_id = kwargs.get('stop_id') 32 | RequiredFieldValidator.validate_field_value('stop_id', self.stop_id) 33 | filter_seconds = kwargs.get('filter_seconds', 10800) # default 10800s => 3hrs 34 | self.latest_valid_time = self.calculate_time_at(seconds=filter_seconds) 35 | 36 | def __call__(self, data): 37 | json_data = json.loads(data) 38 | root_key = next(iter([*json_data]), None) 39 | 40 | if root_key is None: 41 | raise ValueError('root_key: unexpected format') 42 | 43 | arrivals_body = json_data[root_key] 44 | northbound = self.get_arrivals_from_direction_list('Northbound', arrivals_body) 45 | southbound = self.get_arrivals_from_direction_list('Southbound', arrivals_body) 46 | arrivals = northbound + southbound 47 | 48 | transformed_arrivals = [ self.transform_arrival(arrival) for arrival in arrivals ] 49 | filtered_arrivals = [ arrival for arrival in transformed_arrivals if arrival['sched_time'] <= self.latest_valid_time ] 50 | 51 | entities = [ self.__make_trip_update(idx, self.stop_id, arrival) for idx, arrival in enumerate(filtered_arrivals) ] 52 | return FeedMessage.create(entities=entities) 53 | 54 | @classmethod 55 | def get_arrivals_from_direction_list(cls, direction_string, arrivals_body): 56 | arrivals = [] 57 | for direction_list in arrivals_body: 58 | # When there are no arrivals, the API gives us an empty list instead 59 | # of a dictionary 60 | if isinstance(direction_list, dict) \ 61 | and [*direction_list][0] == direction_string: 62 | arrivals.append(direction_list[direction_string]) 63 | if arrivals: 64 | return arrivals[0] 65 | return [] 66 | 67 | @classmethod 68 | def calculate_time_at(cls, **kwargs): 69 | now = pendulum.now() 70 | future_time = now.add(**kwargs) 71 | return int(future_time.timestamp()) 72 | 73 | @classmethod 74 | def to_unix_timestamp(cls, time): 75 | time_obj = pendulum.parse(time, tz=cls.TIMEZONE) 76 | return int(time_obj.timestamp()) 77 | 78 | @classmethod 79 | def transform_arrival(cls, arrival): 80 | transformed_arrival = copy.deepcopy(arrival) 81 | 82 | transformed_arrival['sched_time'] = cls.to_unix_timestamp(arrival['sched_time']) 83 | transformed_arrival['depart_time'] = cls.to_unix_timestamp(arrival['depart_time']) 84 | 85 | return transformed_arrival 86 | 87 | @classmethod 88 | def calculate_realtime(cls, time, status): 89 | matches = cls.STATUS_PATTERN.search(status) 90 | 91 | # If we do not see the pattern ' min', this train is considered On Time 92 | if matches is None: 93 | return time 94 | 95 | delay_in_minutes = int(matches.group('delay')) 96 | return int((pendulum.from_timestamp(time).add(minutes=delay_in_minutes)).timestamp()) 97 | 98 | @classmethod 99 | def __make_trip_update(cls, _id, stop_id, arrival): 100 | entity_id = str(_id + 1) 101 | route_id = cls.ROUTE_ID_LOOKUP.get(arrival['line'], None) 102 | arrival_time = cls.calculate_realtime(arrival['sched_time'], arrival['status']) 103 | departure_time = cls.calculate_realtime(arrival['depart_time'], arrival['status']) 104 | 105 | return TripUpdate.create(entity_id=entity_id, 106 | arrival_time=arrival_time, 107 | departure_time=departure_time, 108 | stop_id=stop_id, 109 | route_id=route_id, 110 | scheduled_arrival_time=arrival['sched_time'], 111 | scheduled_departure_time=arrival['depart_time'], 112 | track=arrival['track'], 113 | headsign=arrival['destination'], 114 | agency_timezone=cls.TIMEZONE) 115 | -------------------------------------------------------------------------------- /test/test_njt_rail.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gtfs_realtime_translators.factories import FeedMessage 4 | from gtfs_realtime_translators.translators import NjtRailGtfsRealtimeTranslator 5 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 6 | 7 | 8 | @pytest.fixture 9 | def njt_rail(): 10 | with open('test/fixtures/njt_rail.xml') as f: 11 | raw = f.read() 12 | return raw 13 | 14 | 15 | def test_njt_data(njt_rail): 16 | translator = NjtRailGtfsRealtimeTranslator() 17 | message = translator(njt_rail) 18 | 19 | entity = message.entity[6] 20 | trip_update = entity.trip_update 21 | stop_time_update = trip_update.stop_time_update[0] 22 | 23 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 24 | assert entity.id == '7' 25 | 26 | assert trip_update.trip.trip_id == '' 27 | assert trip_update.trip.route_id == '10' 28 | 29 | assert stop_time_update.stop_id == 'NP' 30 | assert stop_time_update.departure.time == 1570045710 31 | assert stop_time_update.arrival.time == 1570045710 32 | 33 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 34 | assert intersection_trip_update.headsign == 'New York' 35 | assert intersection_trip_update.route_short_name == 'NEC' 36 | assert intersection_trip_update.route_long_name == 'Northeast Corridor Line' 37 | assert intersection_trip_update.route_color == 'black' 38 | assert intersection_trip_update.route_text_color == 'white' 39 | assert intersection_trip_update.block_id == '3154' 40 | assert intersection_trip_update.agency_timezone == 'America/New_York' 41 | assert intersection_trip_update.custom_status == '' 42 | 43 | intersection_stop_time_update = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 44 | assert intersection_stop_time_update.track == '1' 45 | assert intersection_stop_time_update.scheduled_arrival.time == 1570045710 46 | assert intersection_stop_time_update.scheduled_departure.time == 1570045710 47 | assert intersection_stop_time_update.stop_name == 'Newark Penn' 48 | 49 | 50 | def test_njt_data_amtrak(njt_rail): 51 | translator = NjtRailGtfsRealtimeTranslator() 52 | message = translator(njt_rail) 53 | 54 | entity = message.entity[0] 55 | trip_update = entity.trip_update 56 | stop_time_update = trip_update.stop_time_update[0] 57 | 58 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 59 | assert entity.id == '1' 60 | 61 | assert trip_update.trip.trip_id == '' 62 | assert trip_update.trip.route_id == 'AMTK' 63 | 64 | assert stop_time_update.stop_id == 'NP' 65 | assert stop_time_update.departure.time == 1570044525 66 | assert stop_time_update.arrival.time == 1570044525 67 | 68 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 69 | assert intersection_trip_update.headsign == 'Boston' 70 | assert intersection_trip_update.route_short_name == 'AMTRAK' 71 | assert intersection_trip_update.route_long_name == 'Amtrak' 72 | assert intersection_trip_update.route_color == '#FFFF00' 73 | assert intersection_trip_update.route_text_color == '#000000' 74 | assert intersection_trip_update.block_id == 'A176' 75 | assert intersection_trip_update.agency_timezone == 'America/New_York' 76 | assert intersection_trip_update.custom_status == 'All Aboard' 77 | 78 | intersection_stop_time_update = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 79 | assert intersection_stop_time_update.track == '2' 80 | assert intersection_stop_time_update.scheduled_arrival.time == 1570042920 81 | assert intersection_stop_time_update.scheduled_departure.time == 1570042920 82 | 83 | entity = message.entity[15] 84 | trip_update = entity.trip_update 85 | stop_time_update = trip_update.stop_time_update[0] 86 | 87 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 88 | assert entity.id == '16' 89 | 90 | assert trip_update.trip.trip_id == '' 91 | assert trip_update.trip.route_id == 'AMTK' 92 | 93 | assert stop_time_update.stop_id == 'NP' 94 | assert stop_time_update.departure.time == 1570047420 95 | assert stop_time_update.arrival.time == 1570047420 96 | 97 | intersection_trip_update = trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update] 98 | assert intersection_trip_update.headsign == 'Washington' 99 | assert intersection_trip_update.route_short_name == 'ACELA EXPRESS' 100 | assert intersection_trip_update.route_long_name == 'Amtrak Acela Express' 101 | assert intersection_trip_update.route_color == '#FFFF00' 102 | assert intersection_trip_update.route_text_color == '#000000' 103 | assert intersection_trip_update.block_id == 'A2165' 104 | assert intersection_trip_update.agency_timezone == 'America/New_York' 105 | assert intersection_trip_update.custom_status == '' 106 | 107 | intersection_stop_time_update = stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update] 108 | assert intersection_stop_time_update.track == '3' 109 | assert intersection_stop_time_update.scheduled_arrival.time == 1570047420 110 | assert intersection_stop_time_update.scheduled_departure.time == 1570047420 111 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/njt_bus.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | import xmltodict 3 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 4 | 5 | 6 | class NjtBusGtfsRealtimeTranslator: 7 | 8 | TIMEZONE = 'America/New_York' 9 | 10 | def __init__(self, stop_list): 11 | self.filtered_stops = stop_list.split(',') 12 | if self.filtered_stops is None: 13 | raise ValueError('filtered_stops is required.') 14 | 15 | def __call__(self, data): 16 | station_data = xmltodict.parse(data) 17 | entities = self.__make_trip_updates(station_data, self.filtered_stops) 18 | return FeedMessage.create(entities=entities) 19 | 20 | @classmethod 21 | def __to_unix_time(cls, time): 22 | dt = pendulum.from_format(time, 'DD-MMM-YY HH:mm A', tz=cls.TIMEZONE).in_tz('UTC') 23 | return dt 24 | 25 | @classmethod 26 | def __skip_processing(cls, stop_id, filtered_stops): 27 | # Skip Stop if stop_id is not found 28 | if stop_id is None: 29 | return True 30 | # Skip Stop if stop_id is not in filtered stops 31 | if stop_id not in filtered_stops: 32 | return True 33 | return False 34 | 35 | @classmethod 36 | def __make_trip_updates(cls, data, filtered_stops): 37 | trip_updates = [] 38 | schedule_row_set = data.get('SCHEDULEROWSET') 39 | 40 | trips = [] 41 | if schedule_row_set: 42 | trips = schedule_row_set.values() 43 | 44 | for value in trips: 45 | for idx, item_entry in enumerate(value): 46 | # Intersection Extensions 47 | headsign = item_entry['busheader'] 48 | trip_id = item_entry['gtfs_trip_id'] 49 | route_id = item_entry['gtfs_route_id'] 50 | if item_entry.get('STOP', None) is not None: 51 | stops = item_entry['STOP'] 52 | 53 | # If there is only one for the we have to explicitly create a list 54 | if not isinstance(stops, list): 55 | stops = [stops] 56 | 57 | # Process Stops 58 | for stop in stops: 59 | stop_code = stop['gtfs_stop_Code'] 60 | # Get Stop ID for the given Stop Code From the Mapping 61 | stop_id = NJTBusStopCodeIdMappings.get_stop_id(stop_code) 62 | 63 | if cls.__skip_processing(stop_id, filtered_stops): 64 | continue 65 | 66 | stop_name = stop['stopname'] 67 | track = stop['manual_lane_gate'] 68 | if not track: 69 | track = stop['scheduled_lane_gate'] 70 | 71 | scheduleddeparturedate = stop['scheduleddeparturedate'] 72 | scheduleddeparturetime = stop['scheduleddeparturetime'] 73 | scheduled_datetime = cls.__to_unix_time("{} {}".format(scheduleddeparturedate.title(), scheduleddeparturetime)) 74 | scheduled_departure_time = int(scheduled_datetime.timestamp()) 75 | 76 | sec_late = 0 77 | if stop['sec_late']: 78 | sec_late = int(stop['sec_late']) 79 | arrival_time = int(scheduled_datetime.add(seconds=sec_late).timestamp()) 80 | 81 | trip_update = TripUpdate.create(entity_id=str(idx + 1), 82 | route_id=route_id, 83 | trip_id=trip_id, 84 | stop_id=stop_id, 85 | headsign=headsign, 86 | stop_name=stop_name, 87 | track=track, 88 | arrival_time=arrival_time, 89 | departure_time=arrival_time, 90 | scheduled_departure_time=scheduled_departure_time, 91 | scheduled_arrival_time=scheduled_departure_time, 92 | agency_timezone=cls.TIMEZONE) 93 | trip_updates.append(trip_update) 94 | 95 | return trip_updates 96 | 97 | 98 | class NJTBusStopCodeIdMappings: 99 | 100 | stops = { 101 | '18741':'39786', 102 | '31890':'43283', 103 | '18733':'2240', 104 | '20486':'21066', 105 | '20481':'21128', 106 | '20883':'2916', 107 | '31731':'43248', 108 | '31732':'43249', 109 | '20647':'25584', 110 | '20643':'2859', 111 | '20497':'26641', 112 | '20496':'17082', 113 | '21129':'2866', 114 | '21134':'2867', 115 | '20913':'2873', 116 | '22594':'24570', 117 | '22805':'24434', 118 | '22599':'24982', 119 | '31997':'43469', 120 | '22647':'13697', 121 | '22733':'24090', 122 | '22585':'42132', 123 | '22736':'25104', 124 | '22649':'25179' 125 | } 126 | 127 | @classmethod 128 | def get_stop_id(cls, stop_code): 129 | try: 130 | return cls.stops[stop_code] 131 | except KeyError: 132 | return None 133 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/path_rail.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import warnings 4 | 5 | import pendulum 6 | 7 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 8 | 9 | 10 | class PathGtfsRealtimeTranslatorWarning(Warning): 11 | pass 12 | 13 | 14 | class PathGtfsRealtimeTranslator: 15 | TIMEZONE = 'America/New_York' 16 | 17 | ROUTE_ID_LOOKUP = { 18 | '#4D92FB': '859', 19 | '#65C100': '860', 20 | '#FF9900': '861', 21 | '#D93A30': '862', 22 | '#4D92FB,#FF9900': '1024'} 23 | 24 | STOP_ID_LOOKUP = { 25 | '859_33S_HOB': '781715', 26 | '859_33S_CHR': '781732', 27 | '859_33S_09S': '781734', 28 | '859_33S_14S': '781736', 29 | '859_33S_23S': '781738', 30 | '859_33S_33S': '781740', 31 | '859_HOB_HOB': '781717', 32 | '859_HOB_CHR': '781733', 33 | '859_HOB_09S': '781735', 34 | '859_HOB_14S': '781737', 35 | '859_HOB_23S': '781739', 36 | '859_HOB_33S': '781741', 37 | '860_HOB_HOB': '781715', 38 | '860_HOB_NEW': '781728', 39 | '860_HOB_EXP': '781731', 40 | '860_HOB_WTC': '781763', 41 | '860_WTC_HOB': '781716', 42 | '860_WTC_NEW': '781729', 43 | '860_WTC_EXP': '781730', 44 | '860_WTC_WTC': '781763', 45 | '861_33S_JSQ': '781723', 46 | '861_33S_GRV': '781726', 47 | '861_33S_NEW': '781728', 48 | '861_33S_CHR': '781732', 49 | '861_33S_09S': '781734', 50 | '861_33S_14S': '781736', 51 | '861_33S_23S': '781738', 52 | '861_33S_33S': '781740', 53 | '861_JSQ_JSQ': '781724', 54 | '861_JSQ_GRV': '781727', 55 | '861_JSQ_NEW': '781729', 56 | '861_JSQ_CHR': '781733', 57 | '861_JSQ_09S': '781735', 58 | '861_JSQ_14S': '781737', 59 | '861_JSQ_23S': '781739', 60 | '861_JSQ_33S': '781741', 61 | '862_NWK_NWK': '781719', 62 | '862_NWK_HAR': '781721', 63 | '862_EXP_HAR': '781721', 64 | '862_NWK_JSQ': '781725', 65 | '862_EXP_JSQ': '781725', 66 | '862_NWK_GRV': '781727', 67 | '862_EXP_GRV': '781727', 68 | '862_NWK_EXP': '781731', 69 | '862_EXP_EXP': '781731', 70 | '862_NWK_WTC': '794724', 71 | '862_WTC_NWK': '781718', 72 | '862_EXP_NWK': '781718', 73 | '862_WTC_HAR': '781720', 74 | '862_WTC_JSQ': '781722', 75 | '862_WTC_GRV': '781726', 76 | '862_WTC_EXP': '781730', 77 | '862_WTC_WTC': '794724', 78 | '1024_33S_HOB': '781715', 79 | '1024_33S_JSQ': '781723', 80 | '1024_33S_GRV': '781726', 81 | '1024_33S_NEW': '781728', 82 | '1024_33S_CHR': '781732', 83 | '1024_33S_09S': '781734', 84 | '1024_33S_14S': '781736', 85 | '1024_33S_23S': '781738', 86 | '1024_33S_33S': '781740', 87 | '1024_JSQ_HOB': '781716', 88 | '1024_JSQ_JSQ': '781724', 89 | '1024_JSQ_GRV': '781727', 90 | '1024_JSQ_NEW': '781729', 91 | '1024_JSQ_CHR': '781733', 92 | '1024_JSQ_09S': '781735', 93 | '1024_JSQ_14S': '781737', 94 | '1024_JSQ_23S': '781739', 95 | '1024_JSQ_33S': '781741' 96 | } 97 | 98 | """ 99 | Since PATH GTFS data have stops that are, in most cases, unique to a route, direction, service date, and/or 100 | destination, a mapping is created to ensure we return the appropriate stop_id for a station's arrival updates. 101 | 102 | Keys are based off the line color (a hex value that can be mapped to a GTFS route_id), the destination, and the 103 | current station for which the request has been made. 104 | """ 105 | 106 | def __call__(self, data): 107 | json_data = json.loads(data) 108 | entities = self.__make_trip_updates(json_data) 109 | return FeedMessage.create(entities=entities) 110 | 111 | @classmethod 112 | def __make_trip_updates(cls, data): 113 | trip_updates = [] 114 | now = int(pendulum.now().timestamp()) 115 | 116 | arrivals = data['results'] 117 | for arrival in arrivals: 118 | arrival_updates = arrival['messages'] 119 | for idx, update in enumerate(arrival_updates): 120 | try: 121 | route_id = cls.ROUTE_ID_LOOKUP[update['lineColor']] 122 | destination = arrival['target'] 123 | station = arrival['consideredStation'] 124 | 125 | key = route_id + '_' + destination + '_' + station 126 | stop_id = cls.STOP_ID_LOOKUP[key] 127 | arrival_time = now + math.floor(update['secondsToArrival'] / 60) * 60 128 | headsign = update['headSign'] 129 | 130 | trip_update = TripUpdate.create(entity_id=str(idx + 1), 131 | departure_time=arrival_time, 132 | arrival_time=arrival_time, 133 | route_id=route_id, 134 | stop_id=stop_id, 135 | headsign=headsign, 136 | agency_timezone=cls.TIMEZONE) 137 | trip_updates.append(trip_update) 138 | except KeyError: 139 | warnings.warn(f'Could not generate trip_update for update [{update}] in arrival [{arrival}]', 140 | PathGtfsRealtimeTranslatorWarning) 141 | 142 | return trip_updates -------------------------------------------------------------------------------- /test/test_mbta.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gtfs_realtime_translators.translators import MbtaGtfsRealtimeTranslator 4 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 5 | from gtfs_realtime_translators.factories import FeedMessage 6 | 7 | 8 | @pytest.fixture 9 | def mbta_subway(): 10 | with open('test/fixtures/mbta_subway.json') as f: 11 | raw = f.read() 12 | return raw 13 | 14 | 15 | @pytest.fixture 16 | def mbta_subway_missing_static(): 17 | with open('test/fixtures/mbta_subway_missing_static.json') as f: 18 | raw = f.read() 19 | return raw 20 | 21 | 22 | @pytest.fixture 23 | def mbta_bus(): 24 | with open('test/fixtures/mbta_bus.json') as f: 25 | raw = f.read() 26 | return raw 27 | 28 | 29 | def test_mbta_subway_realtime_arrival(mbta_subway): 30 | translator = MbtaGtfsRealtimeTranslator() 31 | message = translator(mbta_subway) 32 | 33 | entity = message.entity[0] 34 | trip_update = entity.trip_update 35 | stop_time_update = trip_update.stop_time_update[0] 36 | 37 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 38 | 39 | assert entity.id == '1' 40 | assert entity.trip_update.trip.route_id == 'Blue' 41 | assert entity.trip_update.trip.trip_id == '55458948-19:45-GovernmentCenterWonderlandSuspend' 42 | assert stop_time_update.stop_id == '70045' 43 | assert stop_time_update.arrival.time == 1683144315 44 | assert stop_time_update.departure.time == 1683144381 45 | 46 | def test_mbta_bus_realtime_arrival_departure(mbta_bus): 47 | translator = MbtaGtfsRealtimeTranslator() 48 | message = translator(mbta_bus) 49 | 50 | entity = message.entity[0] 51 | trip_update = entity.trip_update 52 | stop_time_update = trip_update.stop_time_update[0] 53 | 54 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 55 | 56 | assert entity.id == '1' 57 | assert entity.trip_update.trip.route_id == '66' 58 | assert entity.trip_update.trip.trip_id == '55800441' 59 | assert stop_time_update.stop_id == '1357' 60 | assert stop_time_update.arrival.time == 1683145580 61 | assert stop_time_update.departure.time == 1683145580 62 | 63 | def test_mbta_bus_realtime_no_arrival_departure(mbta_bus): 64 | translator = MbtaGtfsRealtimeTranslator() 65 | message = translator(mbta_bus) 66 | 67 | entity = message.entity[1] 68 | trip_update = entity.trip_update 69 | stop_time_update = trip_update.stop_time_update[0] 70 | 71 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 72 | 73 | assert entity.id == '2' 74 | assert entity.trip_update.trip.route_id == '66' 75 | assert entity.trip_update.trip.trip_id == '55800443' 76 | assert stop_time_update.stop_id == '1357' 77 | assert stop_time_update.arrival.time == 1683145945 78 | assert stop_time_update.departure.time == 1683145945 79 | 80 | def test_mbta_bus_realtime_arrival_no_departure(mbta_bus): 81 | translator = MbtaGtfsRealtimeTranslator() 82 | message = translator(mbta_bus) 83 | 84 | entity = message.entity[2] 85 | trip_update = entity.trip_update 86 | stop_time_update = trip_update.stop_time_update[0] 87 | 88 | assert message.header.gtfs_realtime_version == FeedMessage.VERSION 89 | 90 | assert entity.id == '3' 91 | assert entity.trip_update.trip.route_id == '66' 92 | assert entity.trip_update.trip.trip_id == '55800446' 93 | assert stop_time_update.stop_id == '1357' 94 | assert stop_time_update.arrival.time == 1683146412 95 | assert stop_time_update.departure.time == 1683146412 96 | 97 | def test_mbta_subway_realtime_include_static_data(mbta_subway): 98 | translator = MbtaGtfsRealtimeTranslator() 99 | message = translator(mbta_subway) 100 | 101 | entity = message.entity[0] 102 | trip_update = entity.trip_update 103 | stop_time_update = trip_update.stop_time_update[0] 104 | 105 | assert trip_update.Extensions[ 106 | intersection_gtfs_realtime.intersection_trip_update].route_short_name == 'BL' 107 | assert trip_update.Extensions[ 108 | intersection_gtfs_realtime.intersection_trip_update].route_long_name == 'Blue Line' 109 | assert trip_update.Extensions[ 110 | intersection_gtfs_realtime.intersection_trip_update].route_color == '003DA5' 111 | assert trip_update.Extensions[ 112 | intersection_gtfs_realtime.intersection_trip_update].route_text_color == 'FFFFFF' 113 | assert trip_update.Extensions[ 114 | intersection_gtfs_realtime.intersection_trip_update].headsign == 'Bowdoin' 115 | assert stop_time_update.Extensions[ 116 | intersection_gtfs_realtime.intersection_stop_time_update].stop_name == 'Maverick' 117 | assert stop_time_update.Extensions[ 118 | intersection_gtfs_realtime.intersection_stop_time_update].scheduled_arrival.time == 1683297180 119 | assert stop_time_update.Extensions[ 120 | intersection_gtfs_realtime.intersection_stop_time_update].scheduled_departure.time == 1683297180 121 | 122 | def test_mbta_subway_realtime_missing_static_data(mbta_subway_missing_static): 123 | translator = MbtaGtfsRealtimeTranslator() 124 | message = translator(mbta_subway_missing_static) 125 | 126 | entity = message.entity[0] 127 | trip_update = entity.trip_update 128 | stop_time_update = trip_update.stop_time_update[0] 129 | 130 | assert trip_update.Extensions[ 131 | intersection_gtfs_realtime.intersection_trip_update].route_short_name == 'BL' 132 | assert trip_update.Extensions[ 133 | intersection_gtfs_realtime.intersection_trip_update].route_long_name == 'Blue Line' 134 | assert trip_update.Extensions[ 135 | intersection_gtfs_realtime.intersection_trip_update].route_color == '003DA5' 136 | assert trip_update.Extensions[ 137 | intersection_gtfs_realtime.intersection_trip_update].route_text_color == 'FFFFFF' 138 | assert trip_update.Extensions[ 139 | intersection_gtfs_realtime.intersection_trip_update].headsign == 'Bowdoin' 140 | assert stop_time_update.Extensions[ 141 | intersection_gtfs_realtime.intersection_stop_time_update].stop_name == '' 142 | assert stop_time_update.Extensions[ 143 | intersection_gtfs_realtime.intersection_stop_time_update].scheduled_arrival.time == 0 144 | assert stop_time_update.Extensions[ 145 | intersection_gtfs_realtime.intersection_stop_time_update].scheduled_departure.time == 0 146 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/factories/factories.py: -------------------------------------------------------------------------------- 1 | from google.transit import gtfs_realtime_pb2 as gtfs_realtime 2 | 3 | from gtfs_realtime_translators.bindings import intersection_pb2 as intersection_gtfs_realtime 4 | 5 | class Entity: 6 | 7 | @staticmethod 8 | def create(entity_id, **kwargs): 9 | return gtfs_realtime.FeedEntity(id=entity_id, **kwargs) 10 | 11 | 12 | class TripUpdate: 13 | 14 | @staticmethod 15 | def __set_stop_time_events(arrival_time, departure_time): 16 | arrival = gtfs_realtime.TripUpdate.StopTimeEvent(time=arrival_time) 17 | if departure_time is None: 18 | departure = arrival 19 | else: 20 | departure = gtfs_realtime.TripUpdate.StopTimeEvent(time=departure_time) 21 | 22 | return arrival, departure 23 | 24 | @staticmethod 25 | def __set_delay_stop_time_events(arrival_delay, departure_delay): 26 | arrival = gtfs_realtime.TripUpdate.StopTimeEvent(delay=arrival_delay) 27 | if departure_delay is None: 28 | departure = arrival 29 | else: 30 | departure = gtfs_realtime.TripUpdate.StopTimeEvent(delay=departure_delay) 31 | return arrival, departure 32 | 33 | @staticmethod 34 | def create(*args, **kwargs): 35 | entity_id = kwargs['entity_id'] 36 | trip_id = kwargs.get('trip_id', None) 37 | route_id = kwargs.get('route_id', None) 38 | stop_id = kwargs['stop_id'] 39 | direction_id = kwargs.get('direction_id', None) 40 | vehicle_id = kwargs.get('vehicle_id', None) 41 | 42 | if 'arrival_delay' in kwargs: 43 | arrival_delay = kwargs.get('arrival_delay',None) 44 | departure_delay = kwargs.get('departure_delay', None) 45 | arrival, departure = TripUpdate.__set_delay_stop_time_events(arrival_delay, departure_delay) 46 | else: 47 | arrival_time = kwargs.get('arrival_time', None) 48 | departure_time = kwargs.get('departure_time', None) 49 | arrival, departure = TripUpdate.__set_stop_time_events(arrival_time, departure_time) 50 | 51 | # Intersection Extensions 52 | headsign = kwargs.get('headsign', None) 53 | track = kwargs.get('track', None) 54 | scheduled_arrival = kwargs.get('scheduled_arrival_time', None) 55 | scheduled_departure = kwargs.get('scheduled_departure_time', None) 56 | stop_name = kwargs.get('stop_name', None) 57 | route_short_name = kwargs.get('route_short_name', None) 58 | route_long_name = kwargs.get('route_long_name', None) 59 | route_color = kwargs.get('route_color', None) 60 | route_text_color = kwargs.get('route_text_color', None) 61 | block_id = kwargs.get('block_id', None) 62 | agency_timezone = kwargs.get('agency_timezone', None) 63 | custom_status = kwargs.get('custom_status', None) 64 | scheduled_interval = kwargs.get('scheduled_interval', None) 65 | route_icon = kwargs.get('route_icon', None) 66 | run_number = kwargs.get('run_number', None) 67 | 68 | trip_descriptor = gtfs_realtime.TripDescriptor(trip_id=trip_id, 69 | route_id=route_id, 70 | direction_id=direction_id) 71 | 72 | stop_time_update = gtfs_realtime.TripUpdate.StopTimeUpdate(arrival=arrival, 73 | departure=departure, 74 | stop_id=stop_id) 75 | 76 | vehicle_descriptor = gtfs_realtime.VehicleDescriptor(id=vehicle_id) 77 | 78 | if track: 79 | stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].track = track 80 | if scheduled_arrival: 81 | stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].scheduled_arrival.time = scheduled_arrival 82 | if scheduled_departure: 83 | stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].scheduled_departure.time = scheduled_departure 84 | if stop_name: 85 | stop_time_update.Extensions[intersection_gtfs_realtime.intersection_stop_time_update].stop_name = stop_name 86 | if run_number: 87 | vehicle_descriptor.Extensions[intersection_gtfs_realtime.intersection_vehicle_descriptor].run_number = run_number 88 | 89 | trip_update = gtfs_realtime.TripUpdate(trip=trip_descriptor, 90 | stop_time_update=[stop_time_update], 91 | vehicle=vehicle_descriptor) 92 | 93 | if headsign: 94 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].headsign = headsign 95 | if route_short_name: 96 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].route_short_name = route_short_name 97 | if route_long_name: 98 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].route_long_name = route_long_name 99 | if route_color: 100 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].route_color = route_color 101 | if route_text_color: 102 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].route_text_color = route_text_color 103 | if block_id: 104 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].block_id = block_id 105 | if agency_timezone: 106 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].agency_timezone = agency_timezone 107 | if custom_status: 108 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].custom_status = custom_status 109 | if scheduled_interval: 110 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].scheduled_interval = scheduled_interval 111 | if route_icon: 112 | trip_update.Extensions[intersection_gtfs_realtime.intersection_trip_update].route_icon = route_icon 113 | 114 | return Entity.create(entity_id, 115 | trip_update=trip_update) 116 | 117 | 118 | class FeedMessage: 119 | VERSION = '2.0' 120 | 121 | @staticmethod 122 | def create(*args, **kwargs): 123 | entities = kwargs['entities'] 124 | header = gtfs_realtime.FeedHeader(gtfs_realtime_version=FeedMessage.VERSION) 125 | message = gtfs_realtime.FeedMessage(header=header, 126 | entity=entities) 127 | return message 128 | -------------------------------------------------------------------------------- /test/fixtures/mbta_subway.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "attributes": { 5 | "arrival_time": "2023-05-03T16:05:15-04:00", 6 | "departure_time": "2023-05-03T16:06:21-04:00", 7 | "direction_id": 0, 8 | "schedule_relationship": null, 9 | "status": null, 10 | "stop_sequence": 70 11 | }, 12 | "id": "prediction-55458948-19:45-GovernmentCenterWonderlandSuspend-70045-70", 13 | "relationships": { 14 | "route": { 15 | "data": { 16 | "id": "Blue", 17 | "type": "route" 18 | } 19 | }, 20 | "schedule": { 21 | "data": { 22 | "id": "schedule_id_70045", 23 | "type": "schedule" 24 | } 25 | }, 26 | "stop": { 27 | "data": { 28 | "id": "70045", 29 | "type": "stop" 30 | } 31 | }, 32 | "trip": { 33 | "data": { 34 | "id": "55458948-19:45-GovernmentCenterWonderlandSuspend", 35 | "type": "trip" 36 | } 37 | }, 38 | "vehicle": { 39 | "data": { 40 | "id": "B-547674D2", 41 | "type": "vehicle" 42 | } 43 | } 44 | }, 45 | "type": "prediction" 46 | } 47 | ], 48 | "included": [ 49 | { 50 | "attributes": { 51 | "arrival_time": "2023-05-05T10:33:00-04:00", 52 | "departure_time": "2023-05-05T10:33:00-04:00", 53 | "direction_id": 0, 54 | "drop_off_type": 0, 55 | "pickup_type": 0, 56 | "stop_headsign": null, 57 | "stop_sequence": 3, 58 | "timepoint": false 59 | }, 60 | "id": "schedule_id_70045", 61 | "relationships": { 62 | "route": { 63 | "data": { 64 | "id": "Blue", 65 | "type": "route" 66 | } 67 | }, 68 | "stop": { 69 | "data": { 70 | "id": "70045", 71 | "type": "stop" 72 | } 73 | }, 74 | "trip": { 75 | "data": { 76 | "id": "55458948-19:45-GovernmentCenterWonderlandSuspend", 77 | "type": "trip" 78 | } 79 | } 80 | }, 81 | "type": "schedule" 82 | }, 83 | { 84 | "attributes": { 85 | "bikes_allowed": 0, 86 | "block_id": "B946_-2", 87 | "direction_id": 0, 88 | "headsign": "Bowdoin", 89 | "name": "", 90 | "wheelchair_accessible": 1 91 | }, 92 | "id": "55458948-19:45-GovernmentCenterWonderlandSuspend", 93 | "links": { 94 | "self": "/trips/55458948-19%3A45-GovernmentCenterWonderlandSuspend" 95 | }, 96 | "relationships": { 97 | "route": { 98 | "data": { 99 | "id": "Blue", 100 | "type": "route" 101 | } 102 | }, 103 | "route_pattern": { 104 | "data": { 105 | "id": "Blue-6-0", 106 | "type": "route_pattern" 107 | } 108 | }, 109 | "service": { 110 | "data": { 111 | "id": "RTL223-1-Wdy-01-GvtCnrWndSsd", 112 | "type": "service" 113 | } 114 | }, 115 | "shape": { 116 | "data": { 117 | "id": "946_0013", 118 | "type": "shape" 119 | } 120 | } 121 | }, 122 | "type": "trip" 123 | }, 124 | { 125 | "attributes": { 126 | "address": null, 127 | "at_street": null, 128 | "description": "Maverick - Blue Line - Bowdoin", 129 | "latitude": 42.369119, 130 | "location_type": 0, 131 | "longitude": -71.03953, 132 | "municipality": "Boston", 133 | "name": "Maverick", 134 | "on_street": null, 135 | "platform_code": null, 136 | "platform_name": "Bowdoin", 137 | "vehicle_type": 1, 138 | "wheelchair_boarding": 1 139 | }, 140 | "id": "70045", 141 | "links": { 142 | "self": "/stops/70045" 143 | }, 144 | "relationships": { 145 | "facilities": { 146 | "links": { 147 | "related": "/facilities/?filter[stop]=70045" 148 | } 149 | }, 150 | "parent_station": { 151 | "data": { 152 | "id": "place-mvbcl", 153 | "type": "stop" 154 | } 155 | }, 156 | "zone": { 157 | "data": { 158 | "id": "RapidTransit", 159 | "type": "zone" 160 | } 161 | } 162 | }, 163 | "type": "stop" 164 | }, 165 | { 166 | "attributes": { 167 | "color": "003DA5", 168 | "description": "Rapid Transit", 169 | "direction_destinations": [ 170 | "Bowdoin", 171 | "Wonderland" 172 | ], 173 | "direction_names": [ 174 | "West", 175 | "East" 176 | ], 177 | "fare_class": "Rapid Transit", 178 | "long_name": "Blue Line", 179 | "short_name": "", 180 | "sort_order": 10040, 181 | "text_color": "FFFFFF", 182 | "type": 1 183 | }, 184 | "id": "Blue", 185 | "links": { 186 | "self": "/routes/Blue" 187 | }, 188 | "relationships": { 189 | "line": { 190 | "data": { 191 | "id": "line-Blue", 192 | "type": "line" 193 | } 194 | } 195 | }, 196 | "type": "route" 197 | } 198 | ], 199 | "jsonapi": { 200 | "version": "1.0" 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/path_rail_new.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 5 | 6 | 7 | class PathRailNewGtfsRealtimeTranslator: 8 | TIMEZONE = 'America/New_York' 9 | 10 | GREY_TRAIN_SERVICE_NUMBER = 0 11 | 12 | SERVICE_LOOKUP = { 13 | '5': { 14 | 'JSQ': 'Journal Square Via Hoboken', 15 | '33S': '33rd St Via Hoboken', 16 | 'HOB': 'Hoboken', 17 | 'route_id': 'ATW' 18 | }, 19 | '4': { 20 | 'WTC': 'World Trade Center', 21 | 'HOB': 'Hoboken', 22 | 'route_id': 'GRE' 23 | }, 24 | '3': { 25 | '33S': '33rd Street', 26 | 'HOB': 'Hoboken', 27 | 'route_id': 'BLU' 28 | }, 29 | '2': { 30 | 'JSQ': 'Journal Square', 31 | '33S': '33rd Street', 32 | 'route_id': 'YEL' 33 | }, 34 | '1': { 35 | 'WTC': 'World Trade Center', 36 | 'NWK': 'Newark', 37 | 'route_id': 'RED' 38 | } 39 | } 40 | STOP_ID_LOOKUP = { 41 | 'GRV': { 42 | "stop_name": 'Grove Street', 43 | "tracks": { 44 | "Tunnel H": '781726', 45 | "Tunnel G": '781727', 46 | } 47 | }, 48 | 'WTC': { 49 | "stop_name": 'World Trade Center', 50 | "tracks": { 51 | "Track 1": '781747T1', 52 | "Track 2": '781747T2', 53 | "Track 3": '781747T3', 54 | "Track 4": '781750T4', 55 | "Track 5": '781750T5', 56 | } 57 | }, 58 | 'NWK': { 59 | "stop_name": 'Newark', 60 | "tracks": { 61 | "Track G": '781719', 62 | "Track H": '781718' 63 | } 64 | }, 65 | 'HAR': { 66 | "stop_name": 'Harrison', 67 | "tracks": { 68 | "Track H": '781720', 69 | "Track G": '781721', 70 | } 71 | }, 72 | 'EXP': { 73 | "stop_name": 'Exchange Place', 74 | "tracks": { 75 | "Tunnel E": '781731', 76 | "Tunnel F": '781730', 77 | } 78 | }, 79 | "NEW": { 80 | "stop_name": 'Newport', 81 | "tracks": { 82 | "Tunnel E": '781728', 83 | "Tunnel F": '781729', 84 | } 85 | }, 86 | "CHR": { 87 | "stop_name": 'Christopher Street', 88 | "tracks": { 89 | "Tunnel B": '781732', 90 | "Tunnel A": '781733' 91 | } 92 | }, 93 | "09S": { 94 | "stop_name": '9th Street', 95 | "tracks": { 96 | "Tunnel B": '781734', 97 | "Tunnel A": '781735', 98 | } 99 | }, 100 | "14S": { 101 | "stop_name": '14th Street', 102 | "tracks": { 103 | "Tunnel B": '781736', 104 | "Tunnel A": '781737', 105 | } 106 | 107 | }, 108 | "23S": { 109 | "stop_name": '23rd Street', 110 | "tracks": { 111 | "Tunnel B": '781738', 112 | "Tunnel A": '781739', 113 | } 114 | }, 115 | "JSQ": { 116 | "stop_name": 'Journal Square', 117 | "tracks": { 118 | "Track 1": '781722', 119 | "Track 2": '781723', 120 | "Track 3": '781724', 121 | "Track 4": '781725', 122 | } 123 | }, 124 | "33S": { 125 | "stop_name": '33rd Street', 126 | "tracks": { 127 | "Track 1": '781742T1', 128 | "Track 2": '781740', 129 | "Track 3": '781742T3', 130 | } 131 | }, 132 | "HOB": { 133 | "stop_name": 'Hoboken', 134 | "tracks": { 135 | "Track 1": '781743', 136 | "Track 2": '781744T2', 137 | "Track 3": '781744T3' 138 | } 139 | } 140 | } 141 | 142 | """ 143 | Since PATH GTFS data have stops that are, in most cases, unique to a route, direction, service date, and/or 144 | destination, a mapping is created to ensure we return the appropriate stop_id/headsign/route information for a stop and track arrival updates. 145 | 146 | Keys are based off the "serviceID" field while also using a second lookup to determine the stop_id based on the track for a particular station 147 | """ 148 | 149 | def __call__(self, data): 150 | json_data = json.loads(data) 151 | entities = self.__make_trip_updates(json_data) 152 | return FeedMessage.create(entities=entities) 153 | 154 | @classmethod 155 | def __to_unix_time(cls, time): 156 | datetime = int(pendulum.from_format( 157 | time, 'HH:mm').in_tz(cls.TIMEZONE).timestamp()) 158 | return datetime 159 | 160 | @classmethod 161 | def __route_lookup(cls, service_id, station_shortkey, track_id, 162 | destination): 163 | service_mapping = cls.SERVICE_LOOKUP.get(str(service_id)) 164 | station_mapping = cls.STOP_ID_LOOKUP.get(station_shortkey) 165 | route_id = service_mapping.get('route_id') 166 | headsign = service_mapping.get(destination) 167 | stop_id = station_mapping.get("tracks").get(track_id) 168 | stop_name = station_mapping.get("stop_name") 169 | return { 170 | 'route_id': route_id, 171 | 'stop_id': stop_id, 172 | 'headsign': headsign, 173 | 'stop_name': stop_name 174 | } 175 | 176 | @classmethod 177 | def __should_skip_update(cls, route_data): 178 | should_skip = False 179 | for key, value in route_data.items(): 180 | if value is None: 181 | should_skip = True 182 | break 183 | return should_skip 184 | 185 | @classmethod 186 | def __is_grey_train(cls, service_id): 187 | return service_id == cls.GREY_TRAIN_SERVICE_NUMBER 188 | 189 | @classmethod 190 | def __make_trip_updates(cls, data): 191 | trip_updates = [] 192 | stations = data['stations'] 193 | for station in stations: 194 | station_shortkey = station['abbrv'] 195 | tracks = station.get('tracks') 196 | if station_shortkey == 'systemwide': 197 | continue 198 | for track in tracks: 199 | track_id = track.get('trackId') 200 | trains = track.get('trains', {}) 201 | for idx, train in enumerate(trains): 202 | if not train.get('trainId'): 203 | continue 204 | train_info = train.get('trainId').split('_') 205 | service_id = train.get('service') 206 | if cls.__is_grey_train(service_id): 207 | continue 208 | destination = train.get('destination') 209 | arrival_time = train.get('depArrTime') 210 | scheduled_arrival_time = cls.__to_unix_time(train_info[0]) 211 | arrival_data = cls.__route_lookup( 212 | service_id, station_shortkey, track_id, destination) 213 | if cls.__should_skip_update(arrival_data): 214 | continue 215 | trip_update = TripUpdate.create( 216 | entity_id=train.get('trainId').strip(), 217 | departure_time=arrival_time, 218 | arrival_time=arrival_time, 219 | scheduled_arrival_time=scheduled_arrival_time, 220 | scheduled_departure_time=scheduled_arrival_time, 221 | track=track_id, 222 | route_id=arrival_data.get( 223 | 'route_id'), 224 | stop_id=arrival_data.get( 225 | 'stop_id'), 226 | headsign=arrival_data.get( 227 | 'headsign'), 228 | stop_name=arrival_data.get('stop_name'), 229 | agency_timezone=cls.TIMEZONE) 230 | trip_updates.append(trip_update) 231 | 232 | return trip_updates 233 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/path_new.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pendulum 3 | 4 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 5 | 6 | 7 | class PathNewGtfsRealtimeTranslator: 8 | TIMEZONE = 'America/New_York' 9 | 10 | GREY_TRAIN_SERVICE_NUMBER = 0 11 | 12 | SERVICE_LOOKUP = { 13 | '5': { 14 | 'JSQ': 'Journal Square Via Hoboken', 15 | '33S': '33rd St Via Hoboken', 16 | 'HOB': 'Hoboken', 17 | 'route_id': '1024' 18 | }, 19 | '4': { 20 | 'WTC': 'World Trade Center', 21 | 'HOB': 'Hoboken', 22 | 'route_id': '860' 23 | }, 24 | '3': { 25 | '33S': '33rd Street', 26 | 'HOB': 'Hoboken', 27 | 'route_id': '859' 28 | }, 29 | '2': { 30 | 'JSQ': 'Journal Square', 31 | '33S': '33rd Street', 32 | 'route_id': '861' 33 | }, 34 | '1': { 35 | 'WTC': 'World Trade Center', 36 | 'NWK': 'Newark', 37 | 'route_id': '862' 38 | } 39 | } 40 | STOP_ID_LOOKUP = { 41 | 'GRV': { 42 | "stop_name": 'Grove Street', 43 | "tracks": { 44 | "Tunnel H": '781726', 45 | "Tunnel G": '781727', 46 | } 47 | }, 48 | 'WTC': { 49 | "stop_name": 'World Trade Center', 50 | "tracks": { 51 | "Track 1": '781763T1', 52 | "Track 2": '781763T2', 53 | "Track 3": '781763T3', 54 | "Track 4": '794724T4', 55 | "Track 5": '794724T5', 56 | } 57 | }, 58 | 'NWK': { 59 | "stop_name": 'Newark', 60 | "tracks": { 61 | "Track G": '781719', 62 | "Track H": '781718' 63 | } 64 | }, 65 | 'HAR': { 66 | "stop_name": 'Harrison', 67 | "tracks": { 68 | "Track H": '781720', 69 | "Track G": '781721', 70 | } 71 | }, 72 | 'EXP': { 73 | "stop_name": 'Exchange Place', 74 | "tracks": { 75 | "Tunnel E": '781731', 76 | "Tunnel F": '781730', 77 | } 78 | }, 79 | "NEW": { 80 | "stop_name": 'Newport', 81 | "tracks": { 82 | "Tunnel E": '781728', 83 | "Tunnel F": '781729', 84 | } 85 | }, 86 | "CHR": { 87 | "stop_name": 'Christopher Street', 88 | "tracks": { 89 | "Tunnel B": '781732', 90 | "Tunnel A": '781733' 91 | } 92 | }, 93 | "09S": { 94 | "stop_name": '9th Street', 95 | "tracks": { 96 | "Tunnel B": '781734', 97 | "Tunnel A": '781735', 98 | } 99 | }, 100 | "14S": { 101 | "stop_name": '14th Street', 102 | "tracks": { 103 | "Tunnel B": '781736', 104 | "Tunnel A": '781737', 105 | } 106 | 107 | }, 108 | "23S": { 109 | "stop_name": '23rd Street', 110 | "tracks": { 111 | "Tunnel B": '781738', 112 | "Tunnel A": '781739', 113 | } 114 | }, 115 | "JSQ": { 116 | "stop_name": 'Journal Square', 117 | "tracks": { 118 | "Track 1": '781722', 119 | "Track 2": '781723', 120 | "Track 3": '781724', 121 | "Track 4": '781725', 122 | } 123 | }, 124 | "33S": { 125 | "stop_name": '33rd Street', 126 | "tracks": { 127 | "Track 1": '781741T1', 128 | "Track 2": '781740', 129 | "Track 3": '781741T3', 130 | } 131 | }, 132 | "HOB": { 133 | "stop_name": 'Hoboken', 134 | "tracks": { 135 | "Track 1": '781717', 136 | "Track 2": '781715', 137 | "Track 3": '781716' 138 | } 139 | } 140 | } 141 | 142 | """ 143 | Since PATH GTFS data have stops that are, in most cases, unique to a route, direction, service date, and/or 144 | destination, a mapping is created to ensure we return the appropriate stop_id/headsign/route information for a stop and track arrival updates. 145 | 146 | Keys are based off the "serviceID" field while also using a second lookup to determine the stop_id based on the track for a particular station 147 | """ 148 | 149 | def __call__(self, data): 150 | json_data = json.loads(data) 151 | entities = self.__make_trip_updates(json_data) 152 | return FeedMessage.create(entities=entities) 153 | 154 | @classmethod 155 | def __to_unix_time(cls, time): 156 | datetime = int(pendulum.from_format( 157 | time, 'HH:mm').in_tz(cls.TIMEZONE).timestamp()) 158 | return datetime 159 | 160 | @classmethod 161 | def __route_lookup(cls, service_id, station_shortkey, track_id, destination): 162 | service_mapping = cls.SERVICE_LOOKUP.get(str(service_id)) 163 | station_mapping = cls.STOP_ID_LOOKUP.get(station_shortkey) 164 | route_id = service_mapping.get('route_id') 165 | headsign = service_mapping.get(destination) 166 | stop_id = station_mapping.get("tracks").get(track_id) 167 | stop_name = station_mapping.get("stop_name") 168 | return { 169 | 'route_id': route_id, 170 | 'stop_id': stop_id, 171 | 'headsign': headsign, 172 | 'stop_name': stop_name 173 | } 174 | 175 | @classmethod 176 | def __should_skip_update(cls, route_data): 177 | should_skip = False 178 | for key, value in route_data.items(): 179 | if value is None: 180 | should_skip = True 181 | break 182 | return should_skip 183 | 184 | @classmethod 185 | def __is_grey_train(cls, service_id): 186 | return service_id == cls.GREY_TRAIN_SERVICE_NUMBER 187 | 188 | @classmethod 189 | def __make_trip_updates(cls, data): 190 | trip_updates = [] 191 | stations = data['stations'] 192 | for station in stations: 193 | station_shortkey = station['abbrv'] 194 | tracks = station.get('tracks') 195 | if station_shortkey == 'systemwide': 196 | continue 197 | for track in tracks: 198 | track_id = track.get('trackId') 199 | trains = track.get('trains', {}) 200 | for idx, train in enumerate(trains): 201 | if not train.get('trainId'): 202 | continue 203 | train_info = train.get('trainId').split('_') 204 | service_id = train.get('service') 205 | if cls.__is_grey_train(service_id): 206 | continue 207 | destination = train.get('destination') 208 | arrival_time = train.get('depArrTime') 209 | scheduled_arrival_time = cls.__to_unix_time(train_info[0]) 210 | arrival_data = cls.__route_lookup( 211 | service_id, station_shortkey, track_id, destination) 212 | if cls.__should_skip_update(arrival_data): 213 | continue 214 | trip_update = TripUpdate.create(entity_id=train.get('trainId').strip(), 215 | departure_time=arrival_time, 216 | arrival_time=arrival_time, 217 | scheduled_arrival_time=scheduled_arrival_time, 218 | scheduled_departure_time=scheduled_arrival_time, 219 | track=track_id, 220 | route_id=arrival_data.get( 221 | 'route_id'), 222 | stop_id=arrival_data.get( 223 | 'stop_id'), 224 | headsign=arrival_data.get( 225 | 'headsign'), 226 | stop_name=arrival_data.get('stop_name'), 227 | agency_timezone=cls.TIMEZONE) 228 | trip_updates.append(trip_update) 229 | 230 | return trip_updates 231 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/mbta.py: -------------------------------------------------------------------------------- 1 | import json 2 | import warnings 3 | 4 | import pendulum 5 | 6 | from gtfs_realtime_translators.factories import TripUpdate, FeedMessage 7 | 8 | 9 | class MbtaGtfsRealtimeTranslator: 10 | TIMEZONE = 'America/New_York' 11 | 12 | def __call__(self, data): 13 | json_data = json.loads(data) 14 | entities = [] 15 | predictions = json_data.get('data') 16 | static_relationships = json_data.get('included') 17 | if predictions and static_relationships: 18 | static_data = self.__get_static_data(static_relationships) 19 | entities = self.__make_trip_updates(predictions, static_data) 20 | return FeedMessage.create(entities=entities) 21 | 22 | @classmethod 23 | def __to_unix_time(cls, time): 24 | return pendulum.parse(time).in_tz(cls.TIMEZONE).int_timestamp 25 | 26 | @classmethod 27 | def __make_trip_updates(cls, predictions, static_data): 28 | trip_updates = [] 29 | for idx, prediction in enumerate(predictions): 30 | entity_id = str(idx + 1) 31 | relationships = prediction['relationships'] 32 | attributes = prediction['attributes'] 33 | stop_id = relationships['stop']['data']['id'] 34 | route_id = relationships['route']['data']['id'] 35 | trip_id = relationships['trip']['data']['id'] 36 | raw_arrival_time = attributes['arrival_time'] 37 | raw_departure_time = attributes['departure_time'] 38 | direction_id = attributes['direction_id'] 39 | 40 | route_color = static_data['routes'][route_id]['color'] 41 | route_text_color = static_data['routes'][route_id]['text_color'] 42 | route_long_name = static_data['routes'][route_id]['long_name'] 43 | route_short_name = static_data['routes'][route_id]['short_name'] 44 | 45 | headsign = static_data['trips'][trip_id]['headsign'] 46 | 47 | scheduled_arrival_time, scheduled_departure_time = \ 48 | cls.__get_scheduled_data(relationships['schedule'].get('data'), 49 | static_data['schedules']) 50 | 51 | stop_name = cls.__get_stop_data(stop_id, 52 | static_data['stops']) 53 | 54 | if cls.__should_capture_prediction(raw_departure_time): 55 | arrival_time, departure_time = cls.__set_arrival_and_departure_times( 56 | raw_arrival_time, raw_departure_time) 57 | trip_update = TripUpdate.create( 58 | entity_id=entity_id, 59 | route_id=route_id, 60 | stop_id=stop_id, 61 | trip_id=trip_id, 62 | arrival_time=arrival_time, 63 | departure_time=departure_time, 64 | direction_id=direction_id, 65 | route_color=route_color, 66 | route_text_color=route_text_color, 67 | route_long_name=route_long_name, 68 | route_short_name=route_short_name, 69 | scheduled_arrival_time=scheduled_arrival_time, 70 | scheduled_departure_time=scheduled_departure_time, 71 | stop_name=stop_name, 72 | headsign=headsign, 73 | agency_timezone=cls.TIMEZONE 74 | ) 75 | trip_updates.append(trip_update) 76 | 77 | return trip_updates 78 | 79 | @classmethod 80 | def __get_static_data(cls, static_relationships): 81 | static_data = {} 82 | for subclass in StaticData.__subclasses__(): 83 | static_data[subclass.NAME] = {} 84 | 85 | for entity in static_relationships: 86 | entity_type = entity['type'] 87 | static_data_type = StaticDataTypeRegistry.get(entity_type) 88 | static_data[static_data_type.NAME][entity['id']] = \ 89 | static_data_type.create_entry(entity['id'], 90 | entity['attributes'], 91 | static_data_type.FIELDS) 92 | return static_data 93 | 94 | @classmethod 95 | def __get_scheduled_data(cls, schedule_relationship, static_schedule_data): 96 | scheduled_arrival_time, scheduled_departure_time = None, None 97 | if schedule_relationship is None: 98 | return scheduled_arrival_time, scheduled_departure_time 99 | 100 | schedule_id = schedule_relationship['id'] 101 | schedule_data = static_schedule_data.get(schedule_id) 102 | 103 | if schedule_data is None: 104 | return scheduled_arrival_time, scheduled_departure_time 105 | 106 | if schedule_data['arrival_time'] is not None: 107 | scheduled_arrival_time = \ 108 | cls.__to_unix_time(schedule_data['arrival_time']) 109 | if schedule_data['departure_time'] is not None: 110 | scheduled_departure_time = \ 111 | cls.__to_unix_time(schedule_data['departure_time']) 112 | return scheduled_arrival_time, scheduled_departure_time 113 | 114 | @classmethod 115 | def __get_stop_data(cls, stop_id, static_stop_data): 116 | stop_data = static_stop_data.get(stop_id) 117 | if stop_data is None: 118 | return None 119 | return stop_data['name'] 120 | 121 | @classmethod 122 | def __set_arrival_and_departure_times(cls, raw_arrival_time, raw_departure_time): 123 | departure_time = cls.__to_unix_time( 124 | raw_departure_time) 125 | if raw_arrival_time: 126 | arrival_time = cls.__to_unix_time( 127 | raw_arrival_time) 128 | else: 129 | arrival_time = departure_time 130 | return arrival_time, departure_time 131 | 132 | @classmethod 133 | def __should_capture_prediction(cls, raw_departure_time): 134 | return raw_departure_time 135 | 136 | 137 | class RouteShortNameTranslate: 138 | ROUTE_ID_SHORT_NAMES = { 139 | 'Green-B': 'GL·B', 140 | 'Green-C': 'GL·C', 141 | 'Green-D': 'GL·D', 142 | 'Green-E': 'GL·E', 143 | 'Blue': 'BL', 144 | 'Red': 'RL', 145 | 'Orange': 'OL', 146 | 'Mattapan': 'M' 147 | } 148 | 149 | def __call__(self, entity_id, attributes_map, field): 150 | if entity_id in self.ROUTE_ID_SHORT_NAMES: 151 | return self.ROUTE_ID_SHORT_NAMES.get(entity_id) 152 | if attributes_map['type'] == '2': 153 | return attributes_map[field].replace(' Line', '') 154 | return attributes_map[field] 155 | 156 | 157 | class IdentityTranslate: 158 | 159 | def __call__(self, entity_id, attributes_map, field): 160 | return attributes_map[field] 161 | 162 | 163 | class StaticData: 164 | """ 165 | Represents a static data `type` from the MBTA API response. Implements 166 | the create_entry() interface. Each instance should contain: 167 | NAME: the key in the map that we create from the static data in the 168 | MBTA API response 169 | FIELDS: the list of fields that we want to extract from the MBTA API 170 | response 171 | FIELD_TRANSLATORS: optional map of fields to their translators 172 | """ 173 | def create_entry(self, entity_id, attributes_map, fields): 174 | """ 175 | Extracts into a map a subset of the fields from the attributes map 176 | contained within each entity in the MBTA API response. 177 | """ 178 | entry = {} 179 | for field in fields: 180 | field_translate = self.get_field_translator(field) 181 | translated_value = field_translate(entity_id, 182 | attributes_map, 183 | field) 184 | entry[field] = translated_value 185 | return entry 186 | 187 | def get_field_translator(self, field): 188 | if not hasattr(self, 'FIELD_TRANSLATORS'): 189 | return IdentityTranslate() 190 | return self.FIELD_TRANSLATORS.get(field, IdentityTranslate()) 191 | 192 | 193 | class Route(StaticData): 194 | NAME = 'routes' 195 | FIELDS = ['color', 'text_color', 'long_name', 'short_name'] 196 | FIELD_TRANSLATORS = { 197 | 'short_name': RouteShortNameTranslate() 198 | } 199 | 200 | 201 | class Stop(StaticData): 202 | NAME = 'stops' 203 | FIELDS = ['name'] 204 | 205 | 206 | class Schedule(StaticData): 207 | NAME = 'schedules' 208 | FIELDS = ['arrival_time', 'departure_time'] 209 | 210 | 211 | class Trip(StaticData): 212 | NAME = 'trips' 213 | FIELDS = ['headsign'] 214 | 215 | 216 | class StaticDataTypeRegistry: 217 | """ 218 | This a map of the possible values in the `type` field and their 219 | corresponding class representations. 220 | """ 221 | TYPES = {'route': Route(), 'stop': Stop(), 'schedule': Schedule(), 222 | 'trip': Trip()} 223 | 224 | @classmethod 225 | def get(cls, key): 226 | if key in cls.TYPES: 227 | return cls.TYPES[key] 228 | else: 229 | warnings.warn(f'No static data type defined for entity type {key}', 230 | StaticDataTypeWarning) 231 | 232 | 233 | class StaticDataTypeWarning(Warning): 234 | pass 235 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/njt_rail_json.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | 3 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 4 | import json 5 | 6 | 7 | class NjtRailJsonGtfsRealtimeTranslator: 8 | """ 9 | This translator accepts data from NJT's proprietary Train Control system. It produces 10 | trip updates similar to GTFS realtime format but with some additional context in the absence of some 11 | required and conditionally required GTFS-RT fields. 12 | 13 | The motivation to produce this format is due to the following: 14 | 1) The APIs do not provide a trip_id 15 | 2) The documentation notes that their realtime data may not always accurately map to their GTFS data 16 | 17 | :param data: JSON formatted realtime feed 18 | 19 | """ 20 | 21 | TIMEZONE = 'America/New_York' 22 | 23 | LINECODE_TO_ROUTE_ID = { 24 | 'AC': '1', 25 | 'SL': '2', 26 | 'ML': '6', 27 | 'BC': '6', 28 | 'ME': '8', 29 | 'GS': '9', 30 | 'NE': '10', 31 | 'PV': '14', 32 | 'PR': '15', 33 | 'RV': '16', 34 | } 35 | 36 | def __init__(self, **kwargs): 37 | self.stop_id = kwargs.get('stop_id') 38 | 39 | def __call__(self, data): 40 | data = json.loads(data) 41 | entities = self.__make_trip_updates(data, self.stop_id) 42 | return FeedMessage.create(entities=entities) 43 | 44 | @classmethod 45 | def __to_unix_time(cls, time): 46 | datetime = pendulum.from_format(time, 'DD-MMM-YYYY HH:mm:ss A', 47 | tz=cls.TIMEZONE).in_tz('UTC') 48 | return datetime 49 | 50 | @classmethod 51 | def __make_trip_updates(cls, data, stop_id): 52 | trip_updates = [] 53 | stop_name = data['STATIONNAME'] 54 | items = data.get('ITEMS', []) 55 | for index, item in enumerate(items): 56 | stops = item.get('STOPS') 57 | if stops: 58 | origin_and_destination = [stops[0], stops[-1]] 59 | route_id = cls.__get_route_id(item, origin_and_destination) 60 | if route_id: 61 | # Intersection Extensions 62 | headsign = item['DESTINATION'] 63 | route_short_name = cls.__get_route_short_name(item) 64 | route_long_name = cls.__get_route_long_name(item, route_id) 65 | route_color = cls.__get_route_color(item, route_id) 66 | route_text_color = cls.__get_route_text_color(item, route_id) 67 | block_id = item['TRAIN_ID'] 68 | track = item['TRACK'] 69 | scheduled_datetime = cls.__to_unix_time(item['SCHED_DEP_DATE']) 70 | departure_time = int(scheduled_datetime.add(seconds=int(item['SEC_LATE'])).timestamp()) 71 | scheduled_departure_time = int(scheduled_datetime.timestamp()) 72 | custom_status = item['STATUS'] 73 | route_icon = cls.__get_route_icon(headsign) 74 | 75 | trip_update = TripUpdate.create(entity_id=str(index + 1), 76 | departure_time=departure_time, 77 | scheduled_departure_time=scheduled_departure_time, 78 | arrival_time=departure_time, 79 | scheduled_arrival_time=scheduled_departure_time, 80 | route_id=route_id, 81 | route_short_name=route_short_name, 82 | route_long_name=route_long_name, 83 | route_color=route_color, 84 | route_text_color=route_text_color, 85 | stop_id=stop_id, 86 | stop_name=stop_name, 87 | headsign=headsign, 88 | track=track, 89 | block_id=block_id, 90 | agency_timezone=cls.TIMEZONE, 91 | custom_status=custom_status, 92 | route_icon=route_icon) 93 | trip_updates.append(trip_update) 94 | 95 | return trip_updates 96 | 97 | @classmethod 98 | def __get_route_id(cls, data, origin_and_destination): 99 | """ 100 | This function resolves route_ids for NJT. 101 | 102 | The algorithm is as follows: 103 | 1) Try to get the route_id based on the line name (or line abbreviation for Amtrak), otherwise... 104 | 2) Try to get the route_id based on the origin and destination, otherwise return None 105 | 106 | For #2, this logic is necessary to discern multiple routes that are mapped to the same line. For instance, the 107 | North Jersey Coast Line operates two different routes. All trains with an origin or destination 108 | of New York Penn Station should resolve to route_id 11 and the others route_id 12 109 | 110 | :param data: keyword args containing data needed to perform the route logic 111 | :param origin_and_destination: an array containing the origin at index 0 and destination at index 1 112 | :return: route_id 113 | """ 114 | route_id = cls.__get_route_id_by_line_code(data) 115 | if route_id: 116 | return route_id 117 | route_id = cls.__get_route_id_by_line_data(data) 118 | if route_id: 119 | return route_id 120 | if origin_and_destination: 121 | return cls.__get_route_id_by_origin_or_destination(data, origin_and_destination) 122 | return None 123 | 124 | @classmethod 125 | def __get_route_id_by_line_code(cls, data): 126 | linecode = data['LINECODE'] 127 | return cls.LINECODE_TO_ROUTE_ID.get(linecode) 128 | 129 | @classmethod 130 | def __get_route_id_by_origin_or_destination(cls, data, origin_and_destination): 131 | origin = origin_and_destination[0] 132 | destination = origin_and_destination[1] 133 | origin_name = origin['STATIONNAME'].replace(' ', '_').lower() 134 | destination_name = destination['STATIONNAME'].replace(' ', '_').lower() 135 | 136 | linecode = data['LINECODE'] 137 | if linecode == 'MC': 138 | hoboken = 'hoboken' 139 | origins_and_destinations = {'denville', 'dover', 'mount_olive', 'lake_hopatcong', 'hackettstown'} 140 | if origin_name == hoboken and destination_name in origins_and_destinations: 141 | return '3' 142 | if origin_name in origins_and_destinations and destination_name == hoboken: 143 | return '3' 144 | return '4' 145 | 146 | if linecode == 'NC': 147 | origins_and_destinations = {'new_york_penn_station'} 148 | if origin_name in origins_and_destinations or destination_name in origins_and_destinations: 149 | return '11' 150 | return '12' 151 | return None 152 | 153 | @classmethod 154 | def __get_route_id_by_line_data(cls, data): 155 | route_id_lookup = { 156 | 'atlantic_city_line': '1', 157 | 'main_line': '6', 158 | 'bergen_county_line': '6', 159 | 'morristown_line': '8', 160 | 'morris_&_essex_line': '8', 161 | 'gladstone_branch': '9', 162 | 'northeast_corridor_line': '10', 163 | 'northeast_corrdr': '10', 164 | 'pascack_valley_line': '14', 165 | 'princeton_branch': '15', 166 | 'raritan_valley_line': '16', 167 | } 168 | 169 | amtrak_route_id = 'AMTK' 170 | if data['LINEABBREVIATION'] == amtrak_route_id: 171 | return amtrak_route_id 172 | 173 | key = data['LINE'].replace(' ', '_').lower() 174 | if 'meadowlands' in key: 175 | route_id = '2' 176 | else: 177 | route_id = route_id_lookup.get(key, None) 178 | return route_id if route_id else None 179 | 180 | @classmethod 181 | def __get_route_long_name(cls, data, route_id): 182 | amtrak_prefix = 'AMTRAK' 183 | abbreviation = data['LINEABBREVIATION'] 184 | if abbreviation == 'AMTK': 185 | return amtrak_prefix.title() if data['LINE'] == amtrak_prefix else f"Amtrak {data['LINE']}".title() 186 | return None 187 | 188 | @classmethod 189 | def __get_route_short_name(cls, data): 190 | if data['LINEABBREVIATION'] == 'AMTK': 191 | return data['LINE'] 192 | return data['LINEABBREVIATION'] 193 | 194 | @classmethod 195 | def __get_route_color(cls, data, route_id): 196 | if route_id == 'AMTK': 197 | return '#FFFF00' 198 | return None 199 | 200 | @classmethod 201 | def __get_route_text_color(cls, data, route_id): 202 | if route_id == 'AMTK': 203 | return '#000000' 204 | return None 205 | 206 | @classmethod 207 | def __get_route_icon(cls, headsign): 208 | 209 | if '✈' in headsign and '-SEC' in headsign: 210 | return 'airport,secaucus' 211 | if '-SEC' in headsign: 212 | return 'secaucus' 213 | if '✈' in headsign: 214 | return 'airport' 215 | return None 216 | -------------------------------------------------------------------------------- /gtfs_realtime_translators/translators/njt_rail.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | 3 | from gtfs_realtime_translators.factories import FeedMessage, TripUpdate 4 | import xmltodict 5 | 6 | 7 | class NjtRailGtfsRealtimeTranslator: 8 | """ 9 | This translator accepts data from NJT's proprietary Train Control system. It produces 10 | trip updates similar to GTFS realtime format but with some additional context in the absence of some 11 | required and conditionally required GTFS-RT fields. 12 | 13 | The motivation to produce this format is due to the following: 14 | 1) The APIs do not provide a trip_id 15 | 2) The documentation notes that their realtime data may not always accurately map to their GTFS data 16 | 17 | :param data: XML formatted realtime feed 18 | 19 | https://usermanual.wiki/Document/NJTRANSIT20REAL20Time20Data20Interface20Instructions2020Ver2025.785373145.pdf 20 | """ 21 | 22 | TIMEZONE = 'America/New_York' 23 | 24 | def __call__(self, data): 25 | station_data = xmltodict.parse(data) 26 | entities = self.__make_trip_updates(station_data) 27 | return FeedMessage.create(entities=entities) 28 | 29 | @classmethod 30 | def __to_unix_time(cls, time): 31 | datetime = pendulum.from_format(time, 'DD-MMM-YYYY HH:mm:ss A', 32 | tz=cls.TIMEZONE).in_tz('UTC') 33 | return datetime 34 | 35 | @classmethod 36 | def __make_trip_updates(cls, data): 37 | trip_updates = [] 38 | 39 | station_data_item = data['STATION']['ITEMS'].values() 40 | for value in station_data_item: 41 | for idx, item_entry in enumerate(value): 42 | origin_and_destination = None 43 | stops = item_entry['STOPS'] 44 | if stops: 45 | for stop in stops.values(): 46 | origin_and_destination = [stop[i] for i in (0, -1)] 47 | 48 | route_id = cls.__get_route_id(item_entry, 49 | origin_and_destination) 50 | if route_id: 51 | # Intersection Extensions 52 | headsign = item_entry['DESTINATION'] 53 | route_short_name = cls.__get_route_short_name( 54 | item_entry) 55 | route_long_name = cls.__get_route_long_name(item_entry) 56 | route_color = cls.__get_route_color(item_entry, 57 | route_id) 58 | route_text_color = cls.__get_route_text_color( 59 | item_entry, route_id) 60 | block_id = item_entry['TRAIN_ID'] 61 | track = item_entry['TRACK'] 62 | stop_id = data['STATION']['STATION_2CHAR'] 63 | stop_name = data['STATION']['STATIONNAME'] 64 | scheduled_datetime = cls.__to_unix_time( 65 | item_entry['SCHED_DEP_DATE']) 66 | departure_time = int(scheduled_datetime.add( 67 | seconds=int(item_entry['SEC_LATE'])).timestamp()) 68 | scheduled_departure_time = int( 69 | scheduled_datetime.timestamp()) 70 | custom_status = item_entry['STATUS'] 71 | route_icon = cls.__get_route_icon(headsign) 72 | 73 | trip_update = TripUpdate.create(entity_id=str(idx + 1), 74 | departure_time=departure_time, 75 | scheduled_departure_time=scheduled_departure_time, 76 | arrival_time=departure_time, 77 | scheduled_arrival_time=scheduled_departure_time, 78 | route_id=route_id, 79 | route_short_name=route_short_name, 80 | route_long_name=route_long_name, 81 | route_color=route_color, 82 | route_text_color=route_text_color, 83 | stop_id=stop_id, 84 | stop_name=stop_name, 85 | headsign=headsign, 86 | track=track, 87 | block_id=block_id, 88 | agency_timezone=cls.TIMEZONE, 89 | custom_status=custom_status, 90 | route_icon=route_icon) 91 | trip_updates.append(trip_update) 92 | 93 | return trip_updates 94 | 95 | @classmethod 96 | def __get_route_id(cls, data, origin_and_destination): 97 | """ 98 | This function resolves route_ids for NJT. 99 | 100 | The algorithm is as follows: 101 | 1) Try to get the route_id based on the line name (or line abbreviation for Amtrak), otherwise... 102 | 2) Try to get the route_id based on the origin and destination, otherwise return None 103 | 104 | For #2, this logic is necessary to discern multiple routes that are mapped to the same line. For instance, the 105 | North Jersey Coast Line operates two different routes. All trains with an origin or destination 106 | of New York Penn Station should resolve to route_id 10 and the others route_id 11 107 | 108 | :param data: keyword args containing data needed to perform the route logic 109 | :param origin_and_destination: an array containing the origin at index 0 and destination at index 1 110 | :return: route_id 111 | """ 112 | 113 | route_id = cls.__get_route_id_by_line_data(data) 114 | if route_id: 115 | return route_id 116 | if origin_and_destination: 117 | return cls.__get_route_id_by_origin_or_destination(data, 118 | origin_and_destination) 119 | return None 120 | 121 | @classmethod 122 | def __get_route_id_by_origin_or_destination(cls, data, 123 | origin_and_destination): 124 | origin = origin_and_destination[0] 125 | destination = origin_and_destination[1] 126 | origin_name = origin['NAME'].replace(' ', '_').lower() 127 | destination_name = destination['NAME'].replace(' ', '_').lower() 128 | 129 | key = data['LINE'].replace(' ', '_').lower() 130 | if 'montclair-boonton' in key: 131 | hoboken = 'hoboken' 132 | origins_and_destinations = {'denville', 'dover', 'mount_olive', 133 | 'lake_hopatcong', 'hackettstown'} 134 | if origin_name == hoboken and destination_name in origins_and_destinations: 135 | return '3' 136 | if origin_name in origins_and_destinations and destination_name == hoboken: 137 | return '3' 138 | return '4' 139 | 140 | if key == 'north_jersey_coast_line': 141 | origins_and_destinations = {'new_york_penn_station'} 142 | if origin_name in origins_and_destinations or destination_name in origins_and_destinations: 143 | return '11' 144 | return '12' 145 | return None 146 | 147 | @classmethod 148 | def __get_route_id_by_line_data(cls, data): 149 | route_id_lookup = { 150 | 'atlantic_city_line': '1', 151 | 'betmgm_meadowlands': '2', 152 | 'main_line': '6', 153 | 'bergen_county_line': '6', 154 | 'morristown_line': '8', 155 | 'morris_&_essex_line': '8', 156 | 'gladstone_branch': '9', 157 | 'northeast_corridor_line': '10', 158 | 'pascack_valley_line': '14', 159 | 'princeton_branch': '15', 160 | 'raritan_valley_line': '16', 161 | } 162 | 163 | amtrak_route_id = 'AMTK' 164 | if data['LINEABBREVIATION'] == amtrak_route_id: 165 | return amtrak_route_id 166 | 167 | key = data['LINE'].replace(' ', '_').lower() 168 | route_id = route_id_lookup.get(key, None) 169 | return route_id if route_id else None 170 | 171 | @classmethod 172 | def __get_route_long_name(cls, data): 173 | amtrak_prefix = 'AMTRAK' 174 | abbreviation = data['LINEABBREVIATION'] 175 | if abbreviation == 'AMTK': 176 | return amtrak_prefix.title() if data['LINE'] == amtrak_prefix else f"Amtrak {data['LINE']}".title() 177 | return None 178 | 179 | @classmethod 180 | def __get_route_short_name(cls, data): 181 | if data['LINEABBREVIATION'] == 'AMTK': 182 | return data['LINE'] 183 | return None 184 | 185 | @classmethod 186 | def __get_route_color(cls, data, route_id): 187 | if route_id == 'AMTK': 188 | return '#FFFF00' 189 | return None 190 | 191 | @classmethod 192 | def __get_route_text_color(cls, data, route_id): 193 | if route_id == 'AMTK': 194 | return '#000000' 195 | return None 196 | 197 | @classmethod 198 | def __get_route_icon(cls, headsign): 199 | 200 | if '✈' in headsign and '-SEC' in headsign: 201 | return 'airport,secaucus' 202 | if '-SEC' in headsign: 203 | return 'secaucus' 204 | if '✈' in headsign: 205 | return 'airport' 206 | return None 207 | -------------------------------------------------------------------------------- /test/fixtures/mta_subway.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "stop": { 3 | "id": "MTASBWY:101", 4 | "name": "Van Cortlandt Park - 242 St", 5 | "lat": 40.889246, 6 | "lon": -73.898584 7 | }, 8 | "groups": [ 9 | { 10 | "route": { 11 | "id": "MTASBWY:1", 12 | "shortName": "1", 13 | "longName": "Broadway - 7 Avenue Local", 14 | "mode": "SUBWAY", 15 | "color": "EE352E", 16 | "agencyName": "MTA New York City Transit", 17 | "paramId": "MTASBWY__1", 18 | "sortOrder": 1, 19 | "routeType": 1, 20 | "regionalFareCardAccepted": false 21 | }, 22 | "times": [ 23 | { 24 | "stopId": "MTASBWY:101N", 25 | "stopName": "Van Cortlandt Park - 242 St", 26 | "stopLat": 40.889246, 27 | "stopLon": -73.898584, 28 | "stopIndex": 36, 29 | "stopCount": 37, 30 | "scheduledArrival": 36720, 31 | "scheduledDeparture": 36720, 32 | "realtimeArrival": 36935, 33 | "realtimeDeparture": 36935, 34 | "arrivalDelay": 215, 35 | "departureDelay": 215, 36 | "timepoint": true, 37 | "realtime": true, 38 | "realtimeState": "UPDATED", 39 | "serviceDay": 1569470400, 40 | "tripId": "MTASBWY:2351", 41 | "tripHeadsign": "Van Cortlandt Park - 242 St", 42 | "arrivalFmt": "2019-09-26T10:15:35-04:00", 43 | "departureFmt": "2019-09-26T10:15:35-04:00", 44 | "stopHeadsign": "242 St", 45 | "track": "4", 46 | "peakOffpeak": 0, 47 | "pattern": { 48 | "id": "MTASBWY:1:0:03", 49 | "desc": "1 to Van Cortlandt Park - 242 St (MTASBWY:101N) from South Ferry (MTASBWY:142N) like trip MTASBWY_1908" 50 | }, 51 | "timestamp": 1569507245, 52 | "directionId": "0", 53 | "realtimeSignText": "", 54 | "regionalFareCardAccepted": false 55 | }, 56 | { 57 | "stopId": "MTASBWY:101N", 58 | "stopName": "Van Cortlandt Park - 242 St", 59 | "stopLat": 40.889246, 60 | "stopLon": -73.898584, 61 | "stopIndex": 36, 62 | "stopCount": 37, 63 | "scheduledArrival": 37200, 64 | "scheduledDeparture": 37200, 65 | "realtimeArrival": 37228, 66 | "realtimeDeparture": 37228, 67 | "arrivalDelay": 28, 68 | "departureDelay": 28, 69 | "timepoint": true, 70 | "realtime": true, 71 | "realtimeState": "UPDATED", 72 | "serviceDay": 1569470400, 73 | "tripId": "MTASBWY:2352", 74 | "tripHeadsign": "Van Cortlandt Park - 242 St", 75 | "arrivalFmt": "2019-09-26T10:20:28-04:00", 76 | "departureFmt": "2019-09-26T10:20:28-04:00", 77 | "stopHeadsign": "242 St", 78 | "track": "4", 79 | "peakOffpeak": 0, 80 | "pattern": { 81 | "id": "MTASBWY:1:0:03", 82 | "desc": "1 to Van Cortlandt Park - 242 St (MTASBWY:101N) from South Ferry (MTASBWY:142N) like trip MTASBWY_1908" 83 | }, 84 | "timestamp": 1569507245, 85 | "directionId": "0", 86 | "realtimeSignText": "", 87 | "regionalFareCardAccepted": false 88 | }, 89 | { 90 | "stopId": "MTASBWY:101N", 91 | "stopName": "Van Cortlandt Park - 242 St", 92 | "stopLat": 40.889246, 93 | "stopLon": -73.898584, 94 | "stopIndex": 36, 95 | "stopCount": 37, 96 | "scheduledArrival": 37470, 97 | "scheduledDeparture": 37470, 98 | "realtimeArrival": 37382, 99 | "realtimeDeparture": 37382, 100 | "arrivalDelay": -88, 101 | "departureDelay": -88, 102 | "timepoint": true, 103 | "realtime": true, 104 | "realtimeState": "UPDATED", 105 | "serviceDay": 1569470400, 106 | "tripId": "MTASBWY:2353", 107 | "tripHeadsign": "Van Cortlandt Park - 242 St", 108 | "arrivalFmt": "2019-09-26T10:23:02-04:00", 109 | "departureFmt": "2019-09-26T10:23:02-04:00", 110 | "stopHeadsign": "242 St", 111 | "track": "4", 112 | "peakOffpeak": 0, 113 | "pattern": { 114 | "id": "MTASBWY:1:0:03", 115 | "desc": "1 to Van Cortlandt Park - 242 St (MTASBWY:101N) from South Ferry (MTASBWY:142N) like trip MTASBWY_1908" 116 | }, 117 | "timestamp": 1569507245, 118 | "directionId": "0", 119 | "realtimeSignText": "", 120 | "regionalFareCardAccepted": false 121 | } 122 | ], 123 | "headsign": "242 St", 124 | "stopHeadsign": true 125 | }, 126 | { 127 | "route": { 128 | "id": "MTASBWY:1", 129 | "shortName": "1", 130 | "longName": "Broadway - 7 Avenue Local", 131 | "mode": "SUBWAY", 132 | "color": "EE352E", 133 | "agencyName": "MTA New York City Transit", 134 | "paramId": "MTASBWY__1", 135 | "sortOrder": 1, 136 | "routeType": 1, 137 | "regionalFareCardAccepted": false 138 | }, 139 | "times": [ 140 | { 141 | "stopId": "MTASBWY:101S", 142 | "stopName": "Van Cortlandt Park - 242 St", 143 | "stopLat": 40.889246, 144 | "stopLon": -73.898584, 145 | "stopIndex": 0, 146 | "stopCount": 37, 147 | "scheduledArrival": 37170, 148 | "scheduledDeparture": 37170, 149 | "realtimeArrival": -1, 150 | "realtimeDeparture": 37170, 151 | "arrivalDelay": -37171, 152 | "departureDelay": 0, 153 | "timepoint": true, 154 | "realtime": true, 155 | "realtimeState": "UPDATED", 156 | "serviceDay": 1569470400, 157 | "tripId": "MTASBWY:5795", 158 | "tripHeadsign": "South Ferry", 159 | "departureFmt": "2019-09-26T10:19:30-04:00", 160 | "stopHeadsign": "Manhattan", 161 | "track": "1", 162 | "peakOffpeak": 0, 163 | "pattern": { 164 | "id": "MTASBWY:1:1:01", 165 | "desc": "1 to South Ferry (MTASBWY:142S) from Van Cortlandt Park - 242 St (MTASBWY:101S) like trip MTASBWY_1460" 166 | }, 167 | "timestamp": 1569507245, 168 | "directionId": "1", 169 | "realtimeSignText": "", 170 | "regionalFareCardAccepted": false 171 | }, 172 | { 173 | "stopId": "MTASBWY:101S", 174 | "stopName": "Van Cortlandt Park - 242 St", 175 | "stopLat": 40.889246, 176 | "stopLon": -73.898584, 177 | "stopIndex": 0, 178 | "stopCount": 37, 179 | "scheduledArrival": 37770, 180 | "scheduledDeparture": 37770, 181 | "realtimeArrival": -1, 182 | "realtimeDeparture": 37770, 183 | "arrivalDelay": -37771, 184 | "departureDelay": 0, 185 | "timepoint": true, 186 | "realtime": true, 187 | "realtimeState": "UPDATED", 188 | "serviceDay": 1569470400, 189 | "tripId": "MTASBWY:5797", 190 | "tripHeadsign": "South Ferry", 191 | "departureFmt": "2019-09-26T10:29:30-04:00", 192 | "stopHeadsign": "Manhattan", 193 | "track": "1", 194 | "peakOffpeak": 0, 195 | "pattern": { 196 | "id": "MTASBWY:1:1:01", 197 | "desc": "1 to South Ferry (MTASBWY:142S) from Van Cortlandt Park - 242 St (MTASBWY:101S) like trip MTASBWY_1460" 198 | }, 199 | "timestamp": 1569507245, 200 | "directionId": "1", 201 | "realtimeSignText": "", 202 | "regionalFareCardAccepted": false 203 | }, 204 | { 205 | "stopId": "MTASBWY:101S", 206 | "stopName": "Van Cortlandt Park - 242 St", 207 | "stopLat": 40.889246, 208 | "stopLon": -73.898584, 209 | "stopIndex": 0, 210 | "stopCount": 37, 211 | "scheduledArrival": 37470, 212 | "scheduledDeparture": 37470, 213 | "realtimeArrival": -1, 214 | "realtimeDeparture": 37470, 215 | "arrivalDelay": -37471, 216 | "departureDelay": 0, 217 | "timepoint": true, 218 | "realtime": true, 219 | "realtimeState": "UPDATED", 220 | "serviceDay": 1569470400, 221 | "tripId": "MTASBWY:5796", 222 | "tripHeadsign": "South Ferry", 223 | "departureFmt": "2019-09-26T10:24:30-04:00", 224 | "stopHeadsign": "Manhattan", 225 | "track": "1", 226 | "peakOffpeak": 0, 227 | "pattern": { 228 | "id": "MTASBWY:1:1:01", 229 | "desc": "1 to South Ferry (MTASBWY:142S) from Van Cortlandt Park - 242 St (MTASBWY:101S) like trip MTASBWY_1460" 230 | }, 231 | "timestamp": 1569507245, 232 | "directionId": "1", 233 | "realtimeSignText": "", 234 | "regionalFareCardAccepted": false 235 | } 236 | ], 237 | "headsign": "Manhattan", 238 | "stopHeadsign": true 239 | } 240 | ] 241 | }] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2018-2019 Intersection 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | --------------------------------------------------------------------------------