├── .digitalocean ├── action_hooks │ ├── build │ ├── deploy │ ├── post_deploy │ ├── post_start_python-2.7 │ ├── post_stop_python-2.7 │ ├── pre_build │ ├── pre_start_python-2.7 │ └── pre_stop_python-2.7 ├── cron │ ├── README.cron │ ├── daily │ │ ├── .gitignore │ │ ├── exportDatasets.sh │ │ ├── trimLogs.py │ │ └── trimLogs.sh │ ├── hourly │ │ ├── .gitignore │ │ └── exportDB.sh │ ├── minutely │ │ ├── .gitignore │ │ └── runTwitterApp.sh │ ├── monthly │ │ └── .gitignore │ └── weekly │ │ ├── README │ │ ├── chrono.dat │ │ ├── chronograph │ │ ├── jobs.allow │ │ └── jobs.deny ├── git_hooks │ └── post-receive └── markers │ ├── README │ └── hot_deploy ├── .gitignore ├── .openshift ├── action_hooks │ ├── build │ ├── deploy │ ├── post_deploy │ ├── post_start_python-2.7 │ ├── post_stop_python-2.7 │ ├── pre_build │ ├── pre_start_python-2.7 │ └── pre_stop_python-2.7 ├── cron │ ├── README.cron │ ├── daily │ │ ├── .gitignore │ │ ├── exportDatasets.sh │ │ └── trimLogs.sh │ ├── hourly │ │ ├── .gitignore │ │ └── exportDB.sh │ ├── minutely │ │ ├── .gitignore │ │ └── runTwitterApp.sh │ ├── monthly │ │ └── .gitignore │ └── weekly │ │ ├── README │ │ ├── chrono.dat │ │ ├── chronograph │ │ ├── jobs.allow │ │ └── jobs.deny └── markers │ └── README ├── ELESAppRunner.py ├── LICENSE ├── README.md ├── analysis ├── WAMU_2014_11_19 │ └── get_rehabbed_units.py ├── __init__.py └── analyzeBreakTimes.py ├── app.py ├── civic.json ├── client ├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── app │ ├── .buildignore │ ├── .htaccess │ ├── 404.html │ ├── favicon.ico │ ├── images │ │ ├── capitol_building.jpg │ │ └── yeoman.png │ ├── index.html │ ├── json │ │ ├── hotcar_reports.json │ │ ├── hotcars_by_day.json │ │ ├── recent_updates.json │ │ ├── station_directory.json │ │ └── units │ │ │ ├── A01E01ESCALATOR.json │ │ │ ├── A01E02ESCALATOR.json │ │ │ ├── A01E03ESCALATOR.json │ │ │ ├── A01E04ESCALATOR.json │ │ │ ├── A01E05ESCALATOR.json │ │ │ ├── A01E06ESCALATOR.json │ │ │ ├── A01E07ESCALATOR.json │ │ │ ├── A01W01ESCALATOR.json │ │ │ ├── A01W02ESCALATOR.json │ │ │ ├── A01W03ESCALATOR.json │ │ │ ├── A01W04ESCALATOR.json │ │ │ ├── A01W05ESCALATOR.json │ │ │ ├── A01W06ESCALATOR.json │ │ │ ├── A01W07ESCALATOR.json │ │ │ ├── A02E01ESCALATOR.json │ │ │ ├── A02E02ESCALATOR.json │ │ │ ├── A02E03ESCALATOR.json │ │ │ ├── A02E04ESCALATOR.json │ │ │ ├── A02E05ESCALATOR.json │ │ │ ├── A02E06ESCALATOR.json │ │ │ ├── A02S01ELEVATOR.json │ │ │ ├── A02S01ESCALATOR.json │ │ │ ├── A02S02ELEVATOR.json │ │ │ ├── A02S02ESCALATOR.json │ │ │ ├── A02S03ESCALATOR.json │ │ │ ├── A02S04ESCALATOR.json │ │ │ ├── A02S05ESCALATOR.json │ │ │ ├── A02W01ESCALATOR.json │ │ │ ├── A02W02ESCALATOR.json │ │ │ ├── A02W03ESCALATOR.json │ │ │ ├── A02W04ESCALATOR.json │ │ │ ├── A02W05ESCALATOR.json │ │ │ ├── A03N01ELEVATOR.json │ │ │ ├── A03N01ESCALATOR.json │ │ │ ├── A03N02ELEVATOR.json │ │ │ ├── A03N02ESCALATOR.json │ │ │ ├── A03N03ELEVATOR.json │ │ │ ├── A03N03ESCALATOR.json │ │ │ ├── A03N04ESCALATOR.json │ │ │ ├── A03N05ESCALATOR.json │ │ │ ├── A03N06ESCALATOR.json │ │ │ ├── A03N07ESCALATOR.json │ │ │ ├── A03S01ESCALATOR.json │ │ │ ├── A03S02ESCALATOR.json │ │ │ ├── A03S03ESCALATOR.json │ │ │ ├── A03S04ESCALATOR.json │ │ │ ├── A03S05ESCALATOR.json │ │ │ ├── A03S06ESCALATOR.json │ │ │ ├── A03S07ESCALATOR.json │ │ │ ├── A04X01ELEVATOR.json │ │ │ ├── A04X01ESCALATOR.json │ │ │ ├── A04X02ELEVATOR.json │ │ │ ├── A04X02ESCALATOR.json │ │ │ ├── A04X03ESCALATOR.json │ │ │ ├── A04X04ESCALATOR.json │ │ │ ├── A04X05ESCALATOR.json │ │ │ ├── A04X06ESCALATOR.json │ │ │ ├── A04X07ESCALATOR.json │ │ │ ├── A04X08ESCALATOR.json │ │ │ ├── A05X01ELEVATOR.json │ │ │ ├── A05X01ESCALATOR.json │ │ │ ├── A05X02ELEVATOR.json │ │ │ ├── A05X02ESCALATOR.json │ │ │ ├── A05X03ESCALATOR.json │ │ │ ├── A05X04ESCALATOR.json │ │ │ ├── A05X05ESCALATOR.json │ │ │ ├── A05X06ESCALATOR.json │ │ │ ├── A06X01ELEVATOR.json │ │ │ ├── A06X01ESCALATOR.json │ │ │ ├── A06X02ELEVATOR.json │ │ │ ├── A06X02ESCALATOR.json │ │ │ ├── A06X03ESCALATOR.json │ │ │ ├── A06X04ESCALATOR.json │ │ │ ├── A06X05ESCALATOR.json │ │ │ ├── A06X06ESCALATOR.json │ │ │ ├── A06X07ESCALATOR.json │ │ │ ├── A06X08ESCALATOR.json │ │ │ ├── A07X01ELEVATOR.json │ │ │ ├── A07X01ESCALATOR.json │ │ │ ├── A07X02ESCALATOR.json │ │ │ ├── A07X03ESCALATOR.json │ │ │ ├── A07X04ESCALATOR.json │ │ │ ├── A07X05ESCALATOR.json │ │ │ ├── A07X06ESCALATOR.json │ │ │ ├── A07X07ESCALATOR.json │ │ │ ├── A07X08ESCALATOR.json │ │ │ ├── A07X09ESCALATOR.json │ │ │ ├── A08N01ELEVATOR.json │ │ │ ├── A08N01ESCALATOR.json │ │ │ ├── A08N02ELEVATOR.json │ │ │ ├── A08N02ESCALATOR.json │ │ │ ├── A08N03ESCALATOR.json │ │ │ ├── A08N04ESCALATOR.json │ │ │ ├── A08N05ESCALATOR.json │ │ │ ├── A08N06ESCALATOR.json │ │ │ ├── A08N07ESCALATOR.json │ │ │ ├── A08N08ESCALATOR.json │ │ │ ├── A08N11ESCALATOR.json │ │ │ ├── A08S01ELEVATOR.json │ │ │ ├── A08S01ESCALATOR.json │ │ │ ├── A08S02ELEVATOR.json │ │ │ ├── A08S02ESCALATOR.json │ │ │ ├── A08S03ELEVATOR.json │ │ │ ├── A08S04ELEVATOR.json │ │ │ ├── A08S05ELEVATOR.json │ │ │ ├── A09X01ELEVATOR.json │ │ │ ├── A09X01ESCALATOR.json │ │ │ ├── A09X02ELEVATOR.json │ │ │ ├── A09X02ESCALATOR.json │ │ │ ├── A09X03ESCALATOR.json │ │ │ ├── A09X04ESCALATOR.json │ │ │ ├── A09X05ESCALATOR.json │ │ │ ├── A09X06ESCALATOR.json │ │ │ ├── A10X01ELEVATOR.json │ │ │ ├── A10X01ESCALATOR.json │ │ │ ├── A10X02ELEVATOR.json │ │ │ ├── A10X02ESCALATOR.json │ │ │ ├── A10X03ESCALATOR.json │ │ │ ├── A10X04ESCALATOR.json │ │ │ ├── A10X05ESCALATOR.json │ │ │ ├── A11X01ELEVATOR.json │ │ │ ├── A11X01ESCALATOR.json │ │ │ ├── A11X02ELEVATOR.json │ │ │ ├── A11X02ESCALATOR.json │ │ │ ├── A11X03ELEVATOR.json │ │ │ ├── A11X03ESCALATOR.json │ │ │ ├── A11X04ELEVATOR.json │ │ │ ├── A11X04ESCALATOR.json │ │ │ ├── A11X05ELEVATOR.json │ │ │ ├── A12X01ELEVATOR.json │ │ │ ├── A12X01ESCALATOR.json │ │ │ ├── A12X02ELEVATOR.json │ │ │ ├── A12X02ESCALATOR.json │ │ │ ├── A12X03ELEVATOR.json │ │ │ ├── A12X03ESCALATOR.json │ │ │ ├── A12X04ESCALATOR.json │ │ │ ├── A13X01ELEVATOR.json │ │ │ ├── A13X01ESCALATOR.json │ │ │ ├── A13X02ELEVATOR.json │ │ │ ├── A13X03ELEVATOR.json │ │ │ ├── A14X01ELEVATOR.json │ │ │ ├── A14X01ESCALATOR.json │ │ │ ├── A14X02ELEVATOR.json │ │ │ ├── A14X02ESCALATOR.json │ │ │ ├── A14X03ELEVATOR.json │ │ │ ├── A15X01ELEVATOR.json │ │ │ ├── A15X01ESCALATOR.json │ │ │ ├── A15X02ELEVATOR.json │ │ │ ├── A15X02ESCALATOR.json │ │ │ ├── A15X03ELEVATOR.json │ │ │ ├── A15X03ESCALATOR.json │ │ │ ├── A15X04ELEVATOR.json │ │ │ ├── A15X05ELEVATOR.json │ │ │ ├── A15X06ELEVATOR.json │ │ │ ├── B01E01ELEVATOR.json │ │ │ ├── B01E01ESCALATOR.json │ │ │ ├── B01E02ELEVATOR.json │ │ │ ├── B01E02ESCALATOR.json │ │ │ ├── B01E03ELEVATOR.json │ │ │ ├── B01E03ESCALATOR.json │ │ │ ├── B01E04ELEVATOR.json │ │ │ ├── B01E04ESCALATOR.json │ │ │ ├── B01E05ESCALATOR.json │ │ │ ├── B01E06ESCALATOR.json │ │ │ ├── B01E07ESCALATOR.json │ │ │ ├── B01E08ESCALATOR.json │ │ │ ├── B01W01ESCALATOR.json │ │ │ ├── B01W02ESCALATOR.json │ │ │ ├── B01W03ESCALATOR.json │ │ │ ├── B01W04ESCALATOR.json │ │ │ ├── B01W05ESCALATOR.json │ │ │ ├── B01W06ESCALATOR.json │ │ │ ├── B02N01ELEVATOR.json │ │ │ ├── B02N01ESCALATOR.json │ │ │ ├── B02N02ELEVATOR.json │ │ │ ├── B02N03ESCALATOR.json │ │ │ ├── B02N04ESCALATOR.json │ │ │ ├── B02N05ESCALATOR.json │ │ │ ├── B02N06ESCALATOR.json │ │ │ ├── B02N07ESCALATOR.json │ │ │ ├── B02S01ESCALATOR.json │ │ │ ├── B02S03ESCALATOR.json │ │ │ ├── B02S04ESCALATOR.json │ │ │ ├── B02S05ESCALATOR.json │ │ │ ├── B02S06ESCALATOR.json │ │ │ ├── B02S07ESCALATOR.json │ │ │ ├── B03N01ELEVATOR.json │ │ │ ├── B03N01ESCALATOR.json │ │ │ ├── B03N02ELEVATOR.json │ │ │ ├── B03N02ESCALATOR.json │ │ │ ├── B03N03ESCALATOR.json │ │ │ ├── B03N04ESCALATOR.json │ │ │ ├── B03S01ESCALATOR.json │ │ │ ├── B03S03ESCALATOR.json │ │ │ ├── B03S04ESCALATOR.json │ │ │ ├── B03S05ESCALATOR.json │ │ │ ├── B03S06ESCALATOR.json │ │ │ ├── B04X01ELEVATOR.json │ │ │ ├── B04X01ESCALATOR.json │ │ │ ├── B04X02ELEVATOR.json │ │ │ ├── B04X02ESCALATOR.json │ │ │ ├── B04X03ELEVATOR.json │ │ │ ├── B04X03ESCALATOR.json │ │ │ ├── B05X01ELEVATOR.json │ │ │ ├── B05X01ESCALATOR.json │ │ │ ├── B05X02ELEVATOR.json │ │ │ ├── B05X02ESCALATOR.json │ │ │ ├── B05X03ESCALATOR.json │ │ │ ├── B05X04ESCALATOR.json │ │ │ ├── B06X01ELEVATOR.json │ │ │ ├── B06X01ESCALATOR.json │ │ │ ├── B06X02ESCALATOR.json │ │ │ ├── B06X03ESCALATOR.json │ │ │ ├── B06X04ESCALATOR.json │ │ │ ├── B07X01ELEVATOR.json │ │ │ ├── B07X01ESCALATOR.json │ │ │ ├── B07X02ESCALATOR.json │ │ │ ├── B07X03ESCALATOR.json │ │ │ ├── B08N04ESCALATOR.json │ │ │ ├── B08N05ESCALATOR.json │ │ │ ├── B08S01ELEVATOR.json │ │ │ ├── B08S01ESCALATOR.json │ │ │ ├── B08S02ESCALATOR.json │ │ │ ├── B08S03ESCALATOR.json │ │ │ ├── B09X01ELEVATOR.json │ │ │ ├── B09X02ELEVATOR.json │ │ │ ├── B09X03ELEVATOR.json │ │ │ ├── B09X04ELEVATOR.json │ │ │ ├── B09X05ELEVATOR.json │ │ │ ├── B09X06ELEVATOR.json │ │ │ ├── B10X01ELEVATOR.json │ │ │ ├── B10X01ESCALATOR.json │ │ │ ├── B10X02ELEVATOR.json │ │ │ ├── B10X02ESCALATOR.json │ │ │ ├── B10X03ELEVATOR.json │ │ │ ├── B10X03ESCALATOR.json │ │ │ ├── B10X04ELEVATOR.json │ │ │ ├── B10X05ELEVATOR.json │ │ │ ├── B11X01ELEVATOR.json │ │ │ ├── B11X01ESCALATOR.json │ │ │ ├── B11X02ELEVATOR.json │ │ │ ├── B11X02ESCALATOR.json │ │ │ ├── B11X03ELEVATOR.json │ │ │ ├── B11X03ESCALATOR.json │ │ │ ├── B11X04ELEVATOR.json │ │ │ ├── B11X04ESCALATOR.json │ │ │ ├── B11X05ELEVATOR.json │ │ │ ├── B11X05ESCALATOR.json │ │ │ ├── B11X06ELEVATOR.json │ │ │ ├── B11X06ESCALATOR.json │ │ │ ├── B11X07ELEVATOR.json │ │ │ ├── B11X07ESCALATOR.json │ │ │ ├── B11X08ELEVATOR.json │ │ │ ├── B35N01ELEVATOR.json │ │ │ ├── B35N01ESCALATOR.json │ │ │ ├── B35N02ELEVATOR.json │ │ │ ├── B35N02ESCALATOR.json │ │ │ ├── B35S01ESCALATOR.json │ │ │ ├── B35S02ESCALATOR.json │ │ │ ├── C01N01ELEVATOR.json │ │ │ ├── C01N01ESCALATOR.json │ │ │ ├── C01N02ELEVATOR.json │ │ │ ├── C01N02ESCALATOR.json │ │ │ ├── C01N03ELEVATOR.json │ │ │ ├── C01N03ESCALATOR.json │ │ │ ├── C01N04ESCALATOR.json │ │ │ ├── C01N05ESCALATOR.json │ │ │ ├── C01S01ESCALATOR.json │ │ │ ├── C01S02ESCALATOR.json │ │ │ ├── C01S03ESCALATOR.json │ │ │ ├── C01S04ESCALATOR.json │ │ │ ├── C01S05ESCALATOR.json │ │ │ ├── C01S06ESCALATOR.json │ │ │ ├── C02E01ELEVATOR.json │ │ │ ├── C02E01ESCALATOR.json │ │ │ ├── C02E02ELEVATOR.json │ │ │ ├── C02E02ESCALATOR.json │ │ │ ├── C02E03ELEVATOR.json │ │ │ ├── C02E03ESCALATOR.json │ │ │ ├── C02E04ESCALATOR.json │ │ │ ├── C02E05ESCALATOR.json │ │ │ ├── C02E06ESCALATOR.json │ │ │ ├── C02E07ESCALATOR.json │ │ │ ├── C02W01ESCALATOR.json │ │ │ ├── C02W02ESCALATOR.json │ │ │ ├── C02W03ESCALATOR.json │ │ │ ├── C02W04ESCALATOR.json │ │ │ ├── C02W05ESCALATOR.json │ │ │ ├── C02W06ESCALATOR.json │ │ │ ├── C02W07ESCALATOR.json │ │ │ ├── C03E01ESCALATOR.json │ │ │ ├── C03E02ESCALATOR.json │ │ │ ├── C03E03ESCALATOR.json │ │ │ ├── C03E04ESCALATOR.json │ │ │ ├── C03E05ESCALATOR.json │ │ │ ├── C03E06ESCALATOR.json │ │ │ ├── C03E07ESCALATOR.json │ │ │ ├── C03W01ELEVATOR.json │ │ │ ├── C03W01ESCALATOR.json │ │ │ ├── C03W02ELEVATOR.json │ │ │ ├── C03W02ESCALATOR.json │ │ │ ├── C03W03ELEVATOR.json │ │ │ ├── C03W03ESCALATOR.json │ │ │ ├── C03W04ESCALATOR.json │ │ │ ├── C03W05ESCALATOR.json │ │ │ ├── C03W06ESCALATOR.json │ │ │ ├── C03W07ESCALATOR.json │ │ │ ├── C04X01ELEVATOR.json │ │ │ ├── C04X01ESCALATOR.json │ │ │ ├── C04X02ELEVATOR.json │ │ │ ├── C04X02ESCALATOR.json │ │ │ ├── C04X03ESCALATOR.json │ │ │ ├── C04X04ESCALATOR.json │ │ │ ├── C04X05ESCALATOR.json │ │ │ ├── C04X06ESCALATOR.json │ │ │ ├── C05E01ELEVATOR.json │ │ │ ├── C05E02ELEVATOR.json │ │ │ ├── C05E03ELEVATOR.json │ │ │ ├── C05W04ELEVATOR.json │ │ │ ├── C05X01ELEVATOR.json │ │ │ ├── C05X01ESCALATOR.json │ │ │ ├── C05X02ELEVATOR.json │ │ │ ├── C05X02ESCALATOR.json │ │ │ ├── C05X03ESCALATOR.json │ │ │ ├── C05X04ESCALATOR.json │ │ │ ├── C05X05ESCALATOR.json │ │ │ ├── C05X06ESCALATOR.json │ │ │ ├── C05X07ESCALATOR.json │ │ │ ├── C05X08ESCALATOR.json │ │ │ ├── C05X09ESCALATOR.json │ │ │ ├── C05X10ESCALATOR.json │ │ │ ├── C06X01ELEVATOR.json │ │ │ ├── C06X01ESCALATOR.json │ │ │ ├── C06X02ELEVATOR.json │ │ │ ├── C06X02ESCALATOR.json │ │ │ ├── C06X03ESCALATOR.json │ │ │ ├── C06X04ESCALATOR.json │ │ │ ├── C06X05ESCALATOR.json │ │ │ ├── C06X06ESCALATOR.json │ │ │ ├── C06X07ESCALATOR.json │ │ │ ├── C06X08ESCALATOR.json │ │ │ ├── C07E04ELEVATOR.json │ │ │ ├── C07E05ELEVATOR.json │ │ │ ├── C07E14ESCALATOR.json │ │ │ ├── C07E15ESCALATOR.json │ │ │ ├── C07N01ELEVATOR.json │ │ │ ├── C07N02ELEVATOR.json │ │ │ ├── C07N03ELEVATOR.json │ │ │ ├── C07N04ESCALATOR.json │ │ │ ├── C07N05ESCALATOR.json │ │ │ ├── C07N06ESCALATOR.json │ │ │ ├── C07N10ESCALATOR.json │ │ │ ├── C07N11ESCALATOR.json │ │ │ ├── C07S07ESCALATOR.json │ │ │ ├── C07S08ESCALATOR.json │ │ │ ├── C07S09ESCALATOR.json │ │ │ ├── C07S12ESCALATOR.json │ │ │ ├── C07S13ESCALATOR.json │ │ │ ├── C08X01ELEVATOR.json │ │ │ ├── C08X01ESCALATOR.json │ │ │ ├── C08X02ELEVATOR.json │ │ │ ├── C08X02ESCALATOR.json │ │ │ ├── C08X03ELEVATOR.json │ │ │ ├── C08X03ESCALATOR.json │ │ │ ├── C08X04ESCALATOR.json │ │ │ ├── C08X05ESCALATOR.json │ │ │ ├── C08X06ESCALATOR.json │ │ │ ├── C08X07ESCALATOR.json │ │ │ ├── C08X08ESCALATOR.json │ │ │ ├── C08X09ESCALATOR.json │ │ │ ├── C08X10ESCALATOR.json │ │ │ ├── C09X01ELEVATOR.json │ │ │ ├── C09X01ESCALATOR.json │ │ │ ├── C09X02ELEVATOR.json │ │ │ ├── C09X02ESCALATOR.json │ │ │ ├── C09X03ELEVATOR.json │ │ │ ├── C09X03ESCALATOR.json │ │ │ ├── C09X04ESCALATOR.json │ │ │ ├── C09X05ESCALATOR.json │ │ │ ├── C09X06ESCALATOR.json │ │ │ ├── C09X07ESCALATOR.json │ │ │ ├── C09X08ESCALATOR.json │ │ │ ├── C09X09ESCALATOR.json │ │ │ ├── C09X10ESCALATOR.json │ │ │ ├── C09X11ESCALATOR.json │ │ │ ├── C09X12ESCALATOR.json │ │ │ ├── C10N01ESCALATOR.json │ │ │ ├── C10N02ESCALATOR.json │ │ │ ├── C10N03ESCALATOR.json │ │ │ ├── C10N04ESCALATOR.json │ │ │ ├── C10S01ELEVATOR.json │ │ │ ├── C10S01ESCALATOR.json │ │ │ ├── C10S02ELEVATOR.json │ │ │ ├── C10S02ESCALATOR.json │ │ │ ├── C10S03ESCALATOR.json │ │ │ ├── C10S04ESCALATOR.json │ │ │ ├── C12X01ELEVATOR.json │ │ │ ├── C12X01ESCALATOR.json │ │ │ ├── C12X02ESCALATOR.json │ │ │ ├── C13N01ELEVATOR.json │ │ │ ├── C13N02ELEVATOR.json │ │ │ ├── C13S01ELEVATOR.json │ │ │ ├── C13S01ESCALATOR.json │ │ │ ├── C13S02ESCALATOR.json │ │ │ ├── C14X01ELEVATOR.json │ │ │ ├── C14X01ESCALATOR.json │ │ │ ├── C14X02ELEVATOR.json │ │ │ ├── C14X02ESCALATOR.json │ │ │ ├── C15N01ELEVATOR.json │ │ │ ├── C15N05ESCALATOR.json │ │ │ ├── C15N06ESCALATOR.json │ │ │ ├── C15S01ELEVATOR.json │ │ │ ├── C15S01ESCALATOR.json │ │ │ ├── C15S02ELEVATOR.json │ │ │ ├── C15S02ESCALATOR.json │ │ │ ├── C15S03ELEVATOR.json │ │ │ ├── C15S03ESCALATOR.json │ │ │ ├── C15S04ELEVATOR.json │ │ │ ├── C15S04ESCALATOR.json │ │ │ ├── D01X01ELEVATOR.json │ │ │ ├── D01X01ESCALATOR.json │ │ │ ├── D01X02ELEVATOR.json │ │ │ ├── D01X02ESCALATOR.json │ │ │ ├── D01X03ELEVATOR.json │ │ │ ├── D01X03ESCALATOR.json │ │ │ ├── D01X04ESCALATOR.json │ │ │ ├── D01X05ESCALATOR.json │ │ │ ├── D01X06ESCALATOR.json │ │ │ ├── D01X07ESCALATOR.json │ │ │ ├── D01X08ESCALATOR.json │ │ │ ├── D01X09ESCALATOR.json │ │ │ ├── D02N01ESCALATOR.json │ │ │ ├── D02N02ESCALATOR.json │ │ │ ├── D02N03ESCALATOR.json │ │ │ ├── D02N04ESCALATOR.json │ │ │ ├── D02N05ESCALATOR.json │ │ │ ├── D02N06ESCALATOR.json │ │ │ ├── D02N07ESCALATOR.json │ │ │ ├── D02S01ELEVATOR.json │ │ │ ├── D02S01ESCALATOR.json │ │ │ ├── D02S02ELEVATOR.json │ │ │ ├── D02S02ESCALATOR.json │ │ │ ├── D02S03ESCALATOR.json │ │ │ ├── D02S04ESCALATOR.json │ │ │ ├── D02S05ESCALATOR.json │ │ │ ├── D02S06ESCALATOR.json │ │ │ ├── D02S07ESCALATOR.json │ │ │ ├── D03E01ESCALATOR.json │ │ │ ├── D03E02ESCALATOR.json │ │ │ ├── D03E03ESCALATOR.json │ │ │ ├── D03E04ESCALATOR.json │ │ │ ├── D03E05ESCALATOR.json │ │ │ ├── D03E06ESCALATOR.json │ │ │ ├── D03E07ESCALATOR.json │ │ │ ├── D03E08ESCALATOR.json │ │ │ ├── D03E09ESCALATOR.json │ │ │ ├── D03W01ESCALATOR.json │ │ │ ├── D03W02ESCALATOR.json │ │ │ ├── D03W03ESCALATOR.json │ │ │ ├── D03W04ELEVATOR.json │ │ │ ├── D03W04ESCALATOR.json │ │ │ ├── D03W05ESCALATOR.json │ │ │ ├── D03W06ESCALATOR.json │ │ │ ├── D03W07ESCALATOR.json │ │ │ ├── D03W08ESCALATOR.json │ │ │ ├── D03W09ESCALATOR.json │ │ │ ├── D04X01ELEVATOR.json │ │ │ ├── D04X01ESCALATOR.json │ │ │ ├── D04X02ELEVATOR.json │ │ │ ├── D04X02ESCALATOR.json │ │ │ ├── D04X03ESCALATOR.json │ │ │ ├── D04X04ESCALATOR.json │ │ │ ├── D04X05ESCALATOR.json │ │ │ ├── D05X01ELEVATOR.json │ │ │ ├── D05X01ESCALATOR.json │ │ │ ├── D05X02ELEVATOR.json │ │ │ ├── D05X02ESCALATOR.json │ │ │ ├── D05X03ESCALATOR.json │ │ │ ├── D05X04ESCALATOR.json │ │ │ ├── D05X05ESCALATOR.json │ │ │ ├── D06X01ELEVATOR.json │ │ │ ├── D06X01ESCALATOR.json │ │ │ ├── D06X02ELEVATOR.json │ │ │ ├── D06X02ESCALATOR.json │ │ │ ├── D06X03ESCALATOR.json │ │ │ ├── D06X04ESCALATOR.json │ │ │ ├── D06X05ESCALATOR.json │ │ │ ├── D07X01ELEVATOR.json │ │ │ ├── D07X01ESCALATOR.json │ │ │ ├── D07X02ELEVATOR.json │ │ │ ├── D07X02ESCALATOR.json │ │ │ ├── D07X03ESCALATOR.json │ │ │ ├── D07X04ESCALATOR.json │ │ │ ├── D07X05ESCALATOR.json │ │ │ ├── D07X06ESCALATOR.json │ │ │ ├── D08N01ESCALATOR.json │ │ │ ├── D08N02ESCALATOR.json │ │ │ ├── D08N03ESCALATOR.json │ │ │ ├── D08N04ESCALATOR.json │ │ │ ├── D08N05ESCALATOR.json │ │ │ ├── D08N06ESCALATOR.json │ │ │ ├── D08S01ELEVATOR.json │ │ │ ├── D08S01ESCALATOR.json │ │ │ ├── D08S02ELEVATOR.json │ │ │ ├── D08S02ESCALATOR.json │ │ │ ├── D08S03ESCALATOR.json │ │ │ ├── D08S04ESCALATOR.json │ │ │ ├── D09X01ELEVATOR.json │ │ │ ├── D09X01ESCALATOR.json │ │ │ ├── D09X02ELEVATOR.json │ │ │ ├── D09X02ESCALATOR.json │ │ │ ├── D09X03ELEVATOR.json │ │ │ ├── D09X03ESCALATOR.json │ │ │ ├── D09X04ELEVATOR.json │ │ │ ├── D09X04ESCALATOR.json │ │ │ ├── D10X01ELEVATOR.json │ │ │ ├── D10X01ESCALATOR.json │ │ │ ├── D10X02ELEVATOR.json │ │ │ ├── D10X02ESCALATOR.json │ │ │ ├── D10X03ESCALATOR.json │ │ │ ├── D10X04ESCALATOR.json │ │ │ ├── D11X01ELEVATOR.json │ │ │ ├── D11X01ESCALATOR.json │ │ │ ├── D11X02ELEVATOR.json │ │ │ ├── D11X02ESCALATOR.json │ │ │ ├── D11X03ELEVATOR.json │ │ │ ├── D11X03ESCALATOR.json │ │ │ ├── D11X04ESCALATOR.json │ │ │ ├── D11X05ESCALATOR.json │ │ │ ├── D11X06ESCALATOR.json │ │ │ ├── D12X01ELEVATOR.json │ │ │ ├── D12X01ESCALATOR.json │ │ │ ├── D12X02ESCALATOR.json │ │ │ ├── D13X01ELEVATOR.json │ │ │ ├── D13X01ESCALATOR.json │ │ │ ├── D13X02ELEVATOR.json │ │ │ ├── D13X02ESCALATOR.json │ │ │ ├── D13X03ELEVATOR.json │ │ │ ├── D13X03ESCALATOR.json │ │ │ ├── D13X04ELEVATOR.json │ │ │ ├── D13X05ELEVATOR.json │ │ │ ├── E01X01ELEVATOR.json │ │ │ ├── E01X01ESCALATOR.json │ │ │ ├── E01X02ELEVATOR.json │ │ │ ├── E01X02ESCALATOR.json │ │ │ ├── E01X03ELEVATOR.json │ │ │ ├── E01X03ESCALATOR.json │ │ │ ├── E01X04ELEVATOR.json │ │ │ ├── E01X04ESCALATOR.json │ │ │ ├── E01X05ELEVATOR.json │ │ │ ├── E01X05ESCALATOR.json │ │ │ ├── E01X06ESCALATOR.json │ │ │ ├── E01X07ESCALATOR.json │ │ │ ├── E02N01ELEVATOR.json │ │ │ ├── E02N01ESCALATOR.json │ │ │ ├── E02N02ELEVATOR.json │ │ │ ├── E02N02ESCALATOR.json │ │ │ ├── E02N03ESCALATOR.json │ │ │ ├── E02N04ESCALATOR.json │ │ │ ├── E02S01ESCALATOR.json │ │ │ ├── E02S02ESCALATOR.json │ │ │ ├── E02S03ESCALATOR.json │ │ │ ├── E02S04ESCALATOR.json │ │ │ ├── E03E01ESCALATOR.json │ │ │ ├── E03E02ESCALATOR.json │ │ │ ├── E03E03ESCALATOR.json │ │ │ ├── E03E04ESCALATOR.json │ │ │ ├── E03W01ELEVATOR.json │ │ │ ├── E03W01ESCALATOR.json │ │ │ ├── E03W02ELEVATOR.json │ │ │ ├── E03W02ESCALATOR.json │ │ │ ├── E03W03ESCALATOR.json │ │ │ ├── E03W04ESCALATOR.json │ │ │ ├── E04X01ELEVATOR.json │ │ │ ├── E04X01ESCALATOR.json │ │ │ ├── E04X02ELEVATOR.json │ │ │ ├── E04X02ESCALATOR.json │ │ │ ├── E04X03ESCALATOR.json │ │ │ ├── E04X04ESCALATOR.json │ │ │ ├── E04X05ESCALATOR.json │ │ │ ├── E04X06ESCALATOR.json │ │ │ ├── E04X07ESCALATOR.json │ │ │ ├── E05X01ELEVATOR.json │ │ │ ├── E05X01ESCALATOR.json │ │ │ ├── E05X02ELEVATOR.json │ │ │ ├── E05X02ESCALATOR.json │ │ │ ├── E05X03ESCALATOR.json │ │ │ ├── E05X04ESCALATOR.json │ │ │ ├── E05X05ESCALATOR.json │ │ │ ├── E05X06ESCALATOR.json │ │ │ ├── E05X07ESCALATOR.json │ │ │ ├── E06X05ESCALATOR.json │ │ │ ├── E06X06ESCALATOR.json │ │ │ ├── E07X01ELEVATOR.json │ │ │ ├── E07X01ESCALATOR.json │ │ │ ├── E07X02ELEVATOR.json │ │ │ ├── E07X02ESCALATOR.json │ │ │ ├── E07X03ESCALATOR.json │ │ │ ├── E07X04ESCALATOR.json │ │ │ ├── E08X01ELEVATOR.json │ │ │ ├── E08X01ESCALATOR.json │ │ │ ├── E08X02ELEVATOR.json │ │ │ ├── E08X02ESCALATOR.json │ │ │ ├── E08X03ELEVATOR.json │ │ │ ├── E08X03ESCALATOR.json │ │ │ ├── E08X04ELEVATOR.json │ │ │ ├── E08X04ESCALATOR.json │ │ │ ├── E08X05ELEVATOR.json │ │ │ ├── E09X01ELEVATOR.json │ │ │ ├── E09X01ESCALATOR.json │ │ │ ├── E09X02ELEVATOR.json │ │ │ ├── E09X02ESCALATOR.json │ │ │ ├── E09X03ELEVATOR.json │ │ │ ├── E09X03ESCALATOR.json │ │ │ ├── E09X04ELEVATOR.json │ │ │ ├── E09X04ESCALATOR.json │ │ │ ├── E09X05ELEVATOR.json │ │ │ ├── E10X01ELEVATOR.json │ │ │ ├── E10X01ESCALATOR.json │ │ │ ├── E10X02ESCALATOR.json │ │ │ ├── F01N01ESCALATOR.json │ │ │ ├── F01N02ESCALATOR.json │ │ │ ├── F01N03ESCALATOR.json │ │ │ ├── F01N04ESCALATOR.json │ │ │ ├── F01N05ESCALATOR.json │ │ │ ├── F01N06ESCALATOR.json │ │ │ ├── F01N07ESCALATOR.json │ │ │ ├── F01N08ESCALATOR.json │ │ │ ├── F01N09ESCALATOR.json │ │ │ ├── F01N10ESCALATOR.json │ │ │ ├── F01N11ESCALATOR.json │ │ │ ├── F01N12ESCALATOR.json │ │ │ ├── F01N13ESCALATOR.json │ │ │ ├── F01N14ESCALATOR.json │ │ │ ├── F01N15ESCALATOR.json │ │ │ ├── F01N16ESCALATOR.json │ │ │ ├── F02X01ELEVATOR.json │ │ │ ├── F02X01ESCALATOR.json │ │ │ ├── F02X02ELEVATOR.json │ │ │ ├── F02X02ESCALATOR.json │ │ │ ├── F02X03ESCALATOR.json │ │ │ ├── F02X04ESCALATOR.json │ │ │ ├── F02X05ESCALATOR.json │ │ │ ├── F03N01ELEVATOR.json │ │ │ ├── F03N01ESCALATOR.json │ │ │ ├── F03N02ELEVATOR.json │ │ │ ├── F03N02ESCALATOR.json │ │ │ ├── F03N03ELEVATOR.json │ │ │ ├── F03N03ESCALATOR.json │ │ │ ├── F03N04ESCALATOR.json │ │ │ ├── F03N05ESCALATOR.json │ │ │ ├── F03N06ESCALATOR.json │ │ │ ├── F03N07ESCALATOR.json │ │ │ ├── F03N08ESCALATOR.json │ │ │ ├── F03N09ESCALATOR.json │ │ │ ├── F03N10ESCALATOR.json │ │ │ ├── F03N11ESCALATOR.json │ │ │ ├── F03N12ESCALATOR.json │ │ │ ├── F03N13ESCALATOR.json │ │ │ ├── F04X01ELEVATOR.json │ │ │ ├── F04X01ESCALATOR.json │ │ │ ├── F04X02ELEVATOR.json │ │ │ ├── F04X02ESCALATOR.json │ │ │ ├── F04X03ESCALATOR.json │ │ │ ├── F04X04ESCALATOR.json │ │ │ ├── F04X05ESCALATOR.json │ │ │ ├── F05E01ELEVATOR.json │ │ │ ├── F05E01ESCALATOR.json │ │ │ ├── F05E02ELEVATOR.json │ │ │ ├── F05E02ESCALATOR.json │ │ │ ├── F05E03ESCALATOR.json │ │ │ ├── F05E04ESCALATOR.json │ │ │ ├── F05W01ELEVATOR.json │ │ │ ├── F05W01ESCALATOR.json │ │ │ ├── F05W02ELEVATOR.json │ │ │ ├── F05W02ESCALATOR.json │ │ │ ├── F05W03ESCALATOR.json │ │ │ ├── F05W04ESCALATOR.json │ │ │ ├── F05W05ESCALATOR.json │ │ │ ├── F06N01ELEVATOR.json │ │ │ ├── F06N01ESCALATOR.json │ │ │ ├── F06N02ESCALATOR.json │ │ │ ├── F06N03ESCALATOR.json │ │ │ ├── F06S01ELEVATOR.json │ │ │ ├── F06S01ESCALATOR.json │ │ │ ├── F06S02ESCALATOR.json │ │ │ ├── F06S03ESCALATOR.json │ │ │ ├── F07X01ELEVATOR.json │ │ │ ├── F07X01ESCALATOR.json │ │ │ ├── F07X02ELEVATOR.json │ │ │ ├── F07X02ESCALATOR.json │ │ │ ├── F07X03ESCALATOR.json │ │ │ ├── F07X04ESCALATOR.json │ │ │ ├── F07X05ESCALATOR.json │ │ │ ├── F07X06ESCALATOR.json │ │ │ ├── F07X07ESCALATOR.json │ │ │ ├── F08X01ELEVATOR.json │ │ │ ├── F08X01ESCALATOR.json │ │ │ ├── F08X02ELEVATOR.json │ │ │ ├── F08X02ESCALATOR.json │ │ │ ├── F08X03ESCALATOR.json │ │ │ ├── F08X04ESCALATOR.json │ │ │ ├── F09X01ELEVATOR.json │ │ │ ├── F09X01ESCALATOR.json │ │ │ ├── F09X02ESCALATOR.json │ │ │ ├── F10X01ELEVATOR.json │ │ │ ├── F10X01ESCALATOR.json │ │ │ ├── F10X02ELEVATOR.json │ │ │ ├── F10X02ESCALATOR.json │ │ │ ├── F10X03ELEVATOR.json │ │ │ ├── F10X03ESCALATOR.json │ │ │ ├── F10X04ESCALATOR.json │ │ │ ├── F11X01ELEVATOR.json │ │ │ ├── F11X01ESCALATOR.json │ │ │ ├── F11X02ESCALATOR.json │ │ │ ├── G01X01ELEVATOR.json │ │ │ ├── G01X01ESCALATOR.json │ │ │ ├── G01X02ELEVATOR.json │ │ │ ├── G01X02ESCALATOR.json │ │ │ ├── G01X03ESCALATOR.json │ │ │ ├── G01X04ESCALATOR.json │ │ │ ├── G01X05ESCALATOR.json │ │ │ ├── G02X01ELEVATOR.json │ │ │ ├── G02X01ESCALATOR.json │ │ │ ├── G02X02ELEVATOR.json │ │ │ ├── G02X02ESCALATOR.json │ │ │ ├── G02X03ESCALATOR.json │ │ │ ├── G02X04ESCALATOR.json │ │ │ ├── G02X05ESCALATOR.json │ │ │ ├── G03X01ELEVATOR.json │ │ │ ├── G03X01ESCALATOR.json │ │ │ ├── G03X02ELEVATOR.json │ │ │ ├── G03X02ESCALATOR.json │ │ │ ├── G03X03ELEVATOR.json │ │ │ ├── G04X01ELEVATOR.json │ │ │ ├── G04X01ESCALATOR.json │ │ │ ├── G04X02ELEVATOR.json │ │ │ ├── G04X02ESCALATOR.json │ │ │ ├── G04X03ESCALATOR.json │ │ │ ├── G05X01ELEVATOR.json │ │ │ ├── G05X01ESCALATOR.json │ │ │ ├── G05X02ELEVATOR.json │ │ │ ├── G05X02ESCALATOR.json │ │ │ ├── G05X03ELEVATOR.json │ │ │ ├── G05X03ESCALATOR.json │ │ │ ├── G05X04ELEVATOR.json │ │ │ ├── G05X04ESCALATOR.json │ │ │ ├── G05X06ELEVATOR.json │ │ │ ├── G05X07ELEVATOR.json │ │ │ ├── J02X01ELEVATOR.json │ │ │ ├── J02X01ESCALATOR.json │ │ │ ├── J02X02ESCALATOR.json │ │ │ ├── J03X01ELEVATOR.json │ │ │ ├── J03X01ESCALATOR.json │ │ │ ├── J03X02ELEVATOR.json │ │ │ ├── J03X02ESCALATOR.json │ │ │ ├── J03X03ESCALATOR.json │ │ │ ├── J03X04ELEVATOR.json │ │ │ ├── J03X04ESCALATOR.json │ │ │ ├── J03X05ELEVATOR.json │ │ │ ├── J03X05ESCALATOR.json │ │ │ ├── J03X06ELEVATOR.json │ │ │ ├── K01X01ELEVATOR.json │ │ │ ├── K01X01ESCALATOR.json │ │ │ ├── K01X02ELEVATOR.json │ │ │ ├── K01X02ESCALATOR.json │ │ │ ├── K01X03ESCALATOR.json │ │ │ ├── K01X04ESCALATOR.json │ │ │ ├── K01X05ESCALATOR.json │ │ │ ├── K01X06ESCALATOR.json │ │ │ ├── K01X07ESCALATOR.json │ │ │ ├── K01X08ESCALATOR.json │ │ │ ├── K02X01ELEVATOR.json │ │ │ ├── K02X01ESCALATOR.json │ │ │ ├── K02X02ELEVATOR.json │ │ │ ├── K02X02ESCALATOR.json │ │ │ ├── K02X03ELEVATOR.json │ │ │ ├── K02X03ESCALATOR.json │ │ │ ├── K02X04ESCALATOR.json │ │ │ ├── K02X05ESCALATOR.json │ │ │ ├── K02X06ESCALATOR.json │ │ │ ├── K02X07ESCALATOR.json │ │ │ ├── K02X08ESCALATOR.json │ │ │ ├── K03X01ELEVATOR.json │ │ │ ├── K03X01ESCALATOR.json │ │ │ ├── K03X02ELEVATOR.json │ │ │ ├── K03X02ESCALATOR.json │ │ │ ├── K03X03ELEVATOR.json │ │ │ ├── K03X03ESCALATOR.json │ │ │ ├── K03X04ESCALATOR.json │ │ │ ├── K03X05ESCALATOR.json │ │ │ ├── K03X06ESCALATOR.json │ │ │ ├── K03X07ESCALATOR.json │ │ │ ├── K04X01ELEVATOR.json │ │ │ ├── K04X01ESCALATOR.json │ │ │ ├── K04X02ELEVATOR.json │ │ │ ├── K04X02ESCALATOR.json │ │ │ ├── K04X03ELEVATOR.json │ │ │ ├── K04X03ESCALATOR.json │ │ │ ├── K04X04ELEVATOR.json │ │ │ ├── K04X04ESCALATOR.json │ │ │ ├── K04X05ELEVATOR.json │ │ │ ├── K04X05ESCALATOR.json │ │ │ ├── K04X06ESCALATOR.json │ │ │ ├── K04X07ESCALATOR.json │ │ │ ├── K04X08ESCALATOR.json │ │ │ ├── K04X09ESCALATOR.json │ │ │ ├── K04X10ESCALATOR.json │ │ │ ├── K04X11ESCALATOR.json │ │ │ ├── K04X12ESCALATOR.json │ │ │ ├── K05X01ELEVATOR.json │ │ │ ├── K05X01ESCALATOR.json │ │ │ ├── K05X02ESCALATOR.json │ │ │ ├── K05X03ESCALATOR.json │ │ │ ├── K06X01ELEVATOR.json │ │ │ ├── K06X01ESCALATOR.json │ │ │ ├── K06X02ELEVATOR.json │ │ │ ├── K06X02ESCALATOR.json │ │ │ ├── K06X03ELEVATOR.json │ │ │ ├── K06X03ESCALATOR.json │ │ │ ├── K06X04ELEVATOR.json │ │ │ ├── K06X04ESCALATOR.json │ │ │ ├── K06X05ELEVATOR.json │ │ │ ├── K06X05ESCALATOR.json │ │ │ ├── K06X06ESCALATOR.json │ │ │ ├── K07X01ELEVATOR.json │ │ │ ├── K07X01ESCALATOR.json │ │ │ ├── K07X02ELEVATOR.json │ │ │ ├── K07X02ESCALATOR.json │ │ │ ├── K07X03ELEVATOR.json │ │ │ ├── K08X01ELEVATOR.json │ │ │ ├── K08X01ESCALATOR.json │ │ │ ├── K08X02ELEVATOR.json │ │ │ ├── K08X02ESCALATOR.json │ │ │ ├── K08X03ELEVATOR.json │ │ │ ├── K08X03ESCALATOR.json │ │ │ ├── K08X04ELEVATOR.json │ │ │ ├── N01X01ELEVATOR.json │ │ │ ├── N01X01ESCALATOR.json │ │ │ ├── N01X02ELEVATOR.json │ │ │ ├── N01X02ESCALATOR.json │ │ │ ├── N01X03ELEVATOR.json │ │ │ ├── N01X03ESCALATOR.json │ │ │ ├── N01X04ELEVATOR.json │ │ │ ├── N01X04ESCALATOR.json │ │ │ ├── N01X05ELEVATOR.json │ │ │ ├── N01X05ESCALATOR.json │ │ │ ├── N01X06ELEVATOR.json │ │ │ ├── N01X06ESCALATOR.json │ │ │ ├── N02X01ELEVATOR.json │ │ │ ├── N02X01ESCALATOR.json │ │ │ ├── N02X02ELEVATOR.json │ │ │ ├── N02X02ESCALATOR.json │ │ │ ├── N02X03ELEVATOR.json │ │ │ ├── N02X03ESCALATOR.json │ │ │ ├── N02X04ELEVATOR.json │ │ │ ├── N02X04ESCALATOR.json │ │ │ ├── N02X05ELEVATOR.json │ │ │ ├── N02X05ESCALATOR.json │ │ │ ├── N02X06ELEVATOR.json │ │ │ ├── N02X06ESCALATOR.json │ │ │ ├── N02X07ESCALATOR.json │ │ │ ├── N02X08ESCALATOR.json │ │ │ ├── N03X01ELEVATOR.json │ │ │ ├── N03X02ELEVATOR.json │ │ │ ├── N03X02ESCALATOR.json │ │ │ ├── N03X03ELEVATOR.json │ │ │ ├── N03X04ELEVATOR.json │ │ │ ├── N03X04ESCALATOR.json │ │ │ ├── N03X05ELEVATOR.json │ │ │ ├── N03X05ESCALATOR.json │ │ │ ├── N03X06ELEVATOR.json │ │ │ ├── N03X06ESCALATOR.json │ │ │ ├── N04X01ELEVATOR.json │ │ │ ├── N04X01ESCALATOR.json │ │ │ ├── N04X02ELEVATOR.json │ │ │ ├── N04X02ESCALATOR.json │ │ │ ├── N04X03ELEVATOR.json │ │ │ ├── N04X04ELEVATOR.json │ │ │ ├── N04X04ESCALATOR.json │ │ │ ├── N04X05ELEVATOR.json │ │ │ ├── N04X05ESCALATOR.json │ │ │ ├── N04X06ELEVATOR.json │ │ │ ├── N04X06ESCALATOR.json │ │ │ ├── N06X01ELEVATOR.json │ │ │ ├── N06X01ESCALATOR.json │ │ │ ├── N06X02ELEVATOR.json │ │ │ ├── N06X02ESCALATOR.json │ │ │ ├── N06X03ELEVATOR.json │ │ │ ├── N06X03ESCALATOR.json │ │ │ ├── N06X04ELEVATOR.json │ │ │ └── N06X04ESCALATOR.json │ ├── robots.txt │ ├── scripts │ │ ├── app.js │ │ ├── controllers │ │ │ ├── about.js │ │ │ ├── dailyservicereport.js │ │ │ ├── datapage.js │ │ │ ├── head.js │ │ │ ├── hotcardirectory.js │ │ │ ├── hotcarpage.js │ │ │ ├── main.js │ │ │ ├── mainjumbotron.js │ │ │ ├── nav.js │ │ │ ├── outages.js │ │ │ ├── press.js │ │ │ ├── rankings.js │ │ │ ├── sitemap.js │ │ │ ├── station.js │ │ │ ├── stationdirectory.js │ │ │ ├── statusentry.js │ │ │ ├── unitentry.js │ │ │ ├── unitpage.js │ │ │ ├── unitperformanceaccordian.js │ │ │ └── unitstatusutils.js │ │ ├── directives │ │ │ ├── affix.js │ │ │ ├── dailyservicereportunitstatustable.js │ │ │ ├── hotcarstempcountplot.js │ │ │ ├── linecolors.js │ │ │ ├── scrolltarget.js │ │ │ ├── scrolltargetcollector.js │ │ │ ├── scrolltobookmark.js │ │ │ ├── tweetline.js │ │ │ ├── unit_calendar_heatmap.js │ │ │ └── voronoi_example.html │ │ ├── filters │ │ │ ├── capfirst.js │ │ │ ├── duration.js │ │ │ ├── percentage.js │ │ │ └── unitidtohuman.js │ │ ├── rankingsSearchString.js │ │ └── services │ │ │ ├── directory.js │ │ │ ├── hotcardirectory.js │ │ │ ├── page.js │ │ │ ├── queryparser.js │ │ │ ├── stationdata.js │ │ │ ├── statustableutils.js │ │ │ ├── unit.js │ │ │ ├── unitservice.js │ │ │ └── unitstatus.js │ ├── sitemap.xml │ ├── styles │ │ ├── animate.css │ │ ├── from_angular_strap.css │ │ ├── github_fork.css │ │ ├── main.css │ │ ├── table.css │ │ └── yeti.bootstrap.min.css │ └── views │ │ ├── about.html │ │ ├── dailyservicereport.html │ │ ├── dailyservicereportunitstatustable.html │ │ ├── dashboard.html │ │ ├── data.html │ │ ├── hotcarpage.html │ │ ├── hotcars.html │ │ ├── main.html │ │ ├── mechanics.html │ │ ├── outages.html │ │ ├── press.html │ │ ├── rankings.html │ │ ├── sitemap.html │ │ ├── station.html │ │ ├── stationlisting.html │ │ ├── tweetlinepartial.html │ │ ├── unit.html │ │ └── unit_calendar_heatmap_partial.html ├── bower.json ├── package.json ├── peg │ ├── pegBuild.py │ ├── rankingsSearchString.js │ └── rankingsSearchString.pegjs ├── site_urls.json └── test │ ├── .jshintrc │ ├── karma.conf.js │ └── spec │ ├── controllers │ ├── about.js │ ├── main.js │ ├── station.js │ ├── stationdirectory.js │ └── unit.js │ ├── directives │ └── linecolors.js │ ├── filters │ ├── duration.js │ └── unitidtohuman.js │ └── services │ ├── directory.js │ ├── statustableutils.js │ └── unitservice.js ├── data ├── codes.tsv ├── data_readme.md ├── elevators.tsv ├── escalators.tsv ├── odbl-10.txt ├── ridership.txt ├── station.names.csv ├── stations.data.csv └── test_data │ ├── data.ods │ ├── data.tsv │ ├── data.txt │ └── data.xlsx ├── dcmetrometrics ├── .test.txt ├── __init__.py ├── common │ ├── DataWriteable.py │ ├── DataWriter.py │ ├── JSONifier.py │ ├── WebJSONMixin.py │ ├── __init__.py │ ├── dbGlobals.py │ ├── descriptors.py │ ├── globals.py │ ├── logging_utils.py │ ├── metroTimes.py │ ├── restartingGreenlet.py │ ├── stations.py │ ├── twitterUtils.py │ └── utils.py ├── eles │ ├── ELESApp.py │ ├── Incident.py │ ├── StatusGroup.py │ ├── WMATA_API.py │ ├── __init__.py │ ├── dbUtils.py │ ├── defs.py │ ├── escalatorRequest.py │ ├── escalatorWebParser.py │ ├── misc_utils.py │ └── models.py ├── hotcars │ ├── __init__.py │ ├── hotCars.py │ ├── models.py │ ├── process_tweets.py │ ├── twitter_api.py │ └── wundergroundAPI.py ├── keys │ ├── __init__.py │ ├── key_utils.py │ └── keys_default.py └── third_party │ ├── LICENSE │ ├── __init__.py │ └── gviz_api.py ├── elevatorApp.py ├── escalatorApp.py ├── hotCarApp.py ├── requirements.txt ├── run_app.sh ├── run_gunicorn.sh ├── serve_gunicorn.py ├── serverApp.py ├── setup.py ├── test ├── __init__.py ├── setup.py ├── status_group.py ├── test_app.py ├── test_exportDB.py └── test_exportdata.py ├── utils ├── __init__.py ├── createStationDatabase.py ├── exportDB.py ├── exportData.py ├── export_data_as_csv.py ├── fixHotCarColors.py ├── generateAllWebPages.py ├── generate_site_map.py ├── importData.py ├── migration.py ├── migrations_eles.py ├── migrations_hotcars.py ├── run_daily_service_reports.py ├── run_recompute_performance_summaries.py ├── trimLogs.py └── utils.py ├── webPageGeneratorApp.py └── wsgi ├── static ├── .gitignore ├── architect.logo.png ├── dailycaller.logo.png ├── dcist.logo.gif ├── hotair.logo.gif ├── statusListing.css ├── styles.css ├── unsuckdcmetro.logo.jpg ├── wapo.logo.png ├── wmata-gallery-place.jpg ├── wmata_ceiling_banner_blue.jpg ├── wmata_ceiling_banner_web.jpg └── wusa9logo.gif └── views ├── DCMetroMetricRedesign ├── dcmetrometric-escalators.html ├── dcmetrometric.html ├── dcmetrometrics-rankings.html ├── escalatorRankings.js ├── rankings.html └── rankingsContent.html ├── data.tpl ├── elevator.tpl ├── elevators.tpl ├── escalator.tpl ├── escalatorOutages.tpl ├── escalatorOutages_js.tpl ├── escalatorRankings.tpl ├── escalatorRankings_js.tpl ├── escalatorRankings_old.tpl ├── escalators.tpl ├── glossary.tpl ├── header.tpl ├── home.tpl ├── hotCar.tpl ├── hotCars.tpl ├── hotCars_js.tpl ├── layout.tpl ├── left_column.tpl ├── old ├── allHotCars.tpl ├── data.tpl ├── escalator.tpl ├── escalatorOutages.tpl ├── escalatorOutages_js.tpl ├── escalatorRankings.tpl ├── escalatorRankings_js.tpl ├── escalatorRankings_old.tpl ├── escalators.tpl ├── footer.tpl ├── glossary.tpl ├── header.tpl ├── home.tpl ├── hotCar.tpl ├── hotCars.tpl ├── hotCars_js.tpl ├── layout.tpl ├── left_column.tpl ├── station.tpl ├── stations.tpl ├── stations_js.tpl └── test.html ├── press.tpl ├── station.tpl ├── stations.tpl ├── stations_js.tpl └── test.html /.digitalocean/action_hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a simple build script and will be executed on your CI system if 3 | # available. Otherwise it will execute while your application is stopped 4 | # before the deploy step. This script gets executed directly, so it 5 | # could be python, php, ruby, etc. 6 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This deploy hook gets executed after dependencies are resolved and the 3 | # build hook has been run but before the application has been started back 4 | # up again. This script gets executed directly, so it could be python, php, 5 | # ruby, etc. 6 | 7 | # Copy private keys into the REPO with each deployment 8 | echo -e "Copying private keys\n" 9 | cp $DATA_DIR/keys_private_digitalocean.py $REPO_DIR/dcmetrometrics/keys/keys.py 10 | 11 | # Install dependencies using pip 12 | if [-e $REPO_DIR/requirements.txt]; then 13 | echo -e 'Installing dependencies using pip\n' 14 | source $PYTHON_DIR/virtenv/bin/activate 15 | pip install -r $REPO_DIR requirements.txt 16 | fi 17 | 18 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/post_deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a simple post deploy hook executed after your application 3 | # is deployed and started. This script gets executed directly, so 4 | # it could be python, php, ruby, etc. 5 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/post_start_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/post_stop_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/pre_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a simple script and will be executed on your CI system if 3 | # available. Otherwise it will execute while your application is stopped 4 | # before the build step. This script gets executed directly, so it 5 | # could be python, php, ruby, etc. 6 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/pre_start_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.digitalocean/action_hooks/pre_stop_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.digitalocean/cron/README.cron: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a periodic basis 2 | ======================================= 3 | Any scripts or jobs added to the minutely, hourly, daily, weekly or monthly 4 | directories will be run on a scheduled basis (frequency is as indicated by the 5 | name of the directory) using run-parts. 6 | 7 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 8 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} 9 | 10 | The presence of two specially named files jobs.deny and jobs.allow controls 11 | how run-parts executes your scripts/jobs. 12 | jobs.deny ===> Prevents specific scripts or jobs from being executed. 13 | jobs.allow ===> Only execute the named scripts or jobs (all other/non-named 14 | scripts that exist in this directory are ignored). 15 | 16 | The principles of jobs.deny and jobs.allow are the same as those of cron.deny 17 | and cron.allow and are described in detail at: 18 | http://docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/ch-Automating_System_Tasks.html#s2-autotasks-cron-access 19 | 20 | See: man crontab or above link for more details and see the the weekly/ 21 | directory for an example. 22 | 23 | -------------------------------------------------------------------------------- /.digitalocean/cron/daily/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.digitalocean/cron/daily/.gitignore -------------------------------------------------------------------------------- /.digitalocean/cron/daily/exportDatasets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Export datasets as .json and .csv files for the 3 | # DC Metro Metrics data webpage. 4 | python $OPENSHIFT_REPO_DIR/utils/exportData.py 5 | python $OPENSHIFT_REPO_DIR/utils/exportDB.py 6 | -------------------------------------------------------------------------------- /.digitalocean/cron/daily/trimLogs.py: -------------------------------------------------------------------------------- 1 | import glob, sys, os 2 | import subprocess 3 | import shutil 4 | 5 | DATA_DIR = os.environ['OPENSHIFT_DATA_DIR'] 6 | os.chdir(DATA_DIR) 7 | 8 | logFiles = glob.glob('*.log') 9 | logFiles = [os.path.abspath(l) for l in logFiles] 10 | 11 | print 'Found %i log files'%len(logFiles) 12 | print '\n'.join(logFiles) 13 | 14 | def trimLog(logFile, numLines = 200000): 15 | path, fn = os.path.split(logFile) 16 | bn, ext = os.path.splitext(fn) 17 | tempFile = '%s.temp'%fn 18 | tempFile = os.path.join(path, tempFile) 19 | 20 | # Execute tail 21 | cmd = ['tail', '-n', str(numLines), logFile] 22 | out = open(tempFile, 'w') 23 | subprocess.call(cmd, stdout = out) 24 | out.close() 25 | 26 | # Remove original log file 27 | os.remove(logFile) 28 | 29 | # Copy the shortened log file 30 | os.rename(tempFile, logFile) 31 | 32 | for logFile in logFiles: 33 | logFile = os.path.abspath(logFile) 34 | trimLog(logFile) 35 | -------------------------------------------------------------------------------- /.digitalocean/cron/daily/trimLogs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Trim the log files 3 | python $OPENSHIFT_REPO_DIR/utils/trimLogs.py 4 | -------------------------------------------------------------------------------- /.digitalocean/cron/hourly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.digitalocean/cron/hourly/.gitignore -------------------------------------------------------------------------------- /.digitalocean/cron/hourly/exportDB.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Dump MongoDB collections as json files 3 | python $OPENSHIFT_REPO_DIR/utils/exportDB.py 4 | 5 | -------------------------------------------------------------------------------- /.digitalocean/cron/minutely/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.digitalocean/cron/minutely/.gitignore -------------------------------------------------------------------------------- /.digitalocean/cron/minutely/runTwitterApp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # runTwitterApp.py now runs in infinite loop. Commenting out cronjob 4 | #source ~/python-2.7/activate_virtenv 5 | #python $OPENSHIFT_REPO_DIR/scripts/runTwitterApp.py 6 | -------------------------------------------------------------------------------- /.digitalocean/cron/monthly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.digitalocean/cron/monthly/.gitignore -------------------------------------------------------------------------------- /.digitalocean/cron/weekly/README: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a weekly basis 2 | ===================================== 3 | Any scripts or jobs added to this directory will be run on a scheduled basis 4 | (weekly) using run-parts. 5 | 6 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 7 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} and handles 8 | the files named jobs.deny and jobs.allow specially. 9 | 10 | In this specific example, the chronograph script is the only script or job file 11 | executed on a weekly basis (due to white-listing it in jobs.allow). And the 12 | README and chrono.dat file are ignored either as a result of being black-listed 13 | in jobs.deny or because they are NOT white-listed in the jobs.allow file. 14 | 15 | For more details, please see ../README.cron file. 16 | 17 | -------------------------------------------------------------------------------- /.digitalocean/cron/weekly/chrono.dat: -------------------------------------------------------------------------------- 1 | Time And Relative D...n In Execution (Open)Shift! 2 | -------------------------------------------------------------------------------- /.digitalocean/cron/weekly/chronograph: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "`date`: `cat $(dirname \"$0\")/chrono.dat`" 4 | -------------------------------------------------------------------------------- /.digitalocean/cron/weekly/jobs.allow: -------------------------------------------------------------------------------- 1 | # 2 | # Script or job files listed in here (one entry per line) will be 3 | # executed on a weekly-basis. 4 | # 5 | # Example: The chronograph script will be executed weekly but the README 6 | # and chrono.dat files in this directory will be ignored. 7 | # 8 | # The README file is actually ignored due to the entry in the 9 | # jobs.deny which is checked before jobs.allow (this file). 10 | # 11 | chronograph 12 | 13 | -------------------------------------------------------------------------------- /.digitalocean/cron/weekly/jobs.deny: -------------------------------------------------------------------------------- 1 | # 2 | # Any script or job files listed in here (one entry per line) will NOT be 3 | # executed (read as ignored by run-parts). 4 | # 5 | 6 | README 7 | 8 | -------------------------------------------------------------------------------- /.digitalocean/git_hooks/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | REPO_DIR=/home/lmendelo/dcmetrometrics/repo 4 | 5 | echo "Repo dir: $REPO_DIR" 6 | git --work-tree=$REPO_DIR --git-dir=/var/repo/dcmetrometrics.git checkout -f 7 | 8 | # Restart the dcmetrometrics background process 9 | sudo service dcmetrometrics restart 10 | 11 | # Restart the gunicorn server which is deploying the app 12 | sudo service gunicorn restart -------------------------------------------------------------------------------- /.digitalocean/markers/README: -------------------------------------------------------------------------------- 1 | Markers 2 | =========== 3 | 4 | Adding marker files to this directory will have the following effects: 5 | 6 | force_clean_build - Will remove the python virtualenv and force rebuild it 7 | including any previously downloaded libraries 8 | 9 | hot_deploy - Support not yet there - coming soon to a theatre near you. 10 | Will enable the dynamic reloading of python scripts. 11 | 12 | disable_auto_scaling - Will prevent scalable applications from scaling up 13 | or down according to application load. 14 | -------------------------------------------------------------------------------- /.digitalocean/markers/hot_deploy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.digitalocean/markers/hot_deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dcmetrometrics/keys/keys.py 3 | dcmetrometrics/keys/keys_private.py 4 | dcmetrometrics/keys/keys_private_digitalocean.py 5 | data/json 6 | data/webpages 7 | .private 8 | .private/* 9 | *swp 10 | *log 11 | venv 12 | www 13 | -------------------------------------------------------------------------------- /.openshift/action_hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a simple build script and will be executed on your CI system if 3 | # available. Otherwise it will execute while your application is stopped 4 | # before the deploy step. This script gets executed directly, so it 5 | # could be python, php, ruby, etc. 6 | -------------------------------------------------------------------------------- /.openshift/action_hooks/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This deploy hook gets executed after dependencies are resolved and the 3 | # build hook has been run but before the application has been started back 4 | # up again. This script gets executed directly, so it could be python, php, 5 | # ruby, etc. 6 | 7 | cp $OPENSHIFT_DATA_DIR/keys_private.py $OPENSHIFT_REPO_DIR/dcmetrometrics/keys/keys.py 8 | -------------------------------------------------------------------------------- /.openshift/action_hooks/post_deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a simple post deploy hook executed after your application 3 | # is deployed and started. This script gets executed directly, so 4 | # it could be python, php, ruby, etc. 5 | -------------------------------------------------------------------------------- /.openshift/action_hooks/post_start_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.openshift/action_hooks/post_stop_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.openshift/action_hooks/pre_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a simple script and will be executed on your CI system if 3 | # available. Otherwise it will execute while your application is stopped 4 | # before the build step. This script gets executed directly, so it 5 | # could be python, php, ruby, etc. 6 | -------------------------------------------------------------------------------- /.openshift/action_hooks/pre_start_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.openshift/action_hooks/pre_stop_python-2.7: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The pre_start_cartridge and pre_stop_cartridge hooks are *SOURCED* 4 | # immediately before (re)starting or stopping the specified cartridge. 5 | # They are able to make any desired environment variable changes as 6 | # well as other adjustments to the application environment. 7 | 8 | # The post_start_cartridge and post_stop_cartridge hooks are executed 9 | # immediately after (re)starting or stopping the specified cartridge. 10 | 11 | # Exercise caution when adding commands to these hooks. They can 12 | # prevent your application from stopping cleanly or starting at all. 13 | # Application start and stop is subject to different timeouts 14 | # throughout the system. 15 | -------------------------------------------------------------------------------- /.openshift/cron/README.cron: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a periodic basis 2 | ======================================= 3 | Any scripts or jobs added to the minutely, hourly, daily, weekly or monthly 4 | directories will be run on a scheduled basis (frequency is as indicated by the 5 | name of the directory) using run-parts. 6 | 7 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 8 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} 9 | 10 | The presence of two specially named files jobs.deny and jobs.allow controls 11 | how run-parts executes your scripts/jobs. 12 | jobs.deny ===> Prevents specific scripts or jobs from being executed. 13 | jobs.allow ===> Only execute the named scripts or jobs (all other/non-named 14 | scripts that exist in this directory are ignored). 15 | 16 | The principles of jobs.deny and jobs.allow are the same as those of cron.deny 17 | and cron.allow and are described in detail at: 18 | http://docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/ch-Automating_System_Tasks.html#s2-autotasks-cron-access 19 | 20 | See: man crontab or above link for more details and see the the weekly/ 21 | directory for an example. 22 | 23 | -------------------------------------------------------------------------------- /.openshift/cron/daily/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.openshift/cron/daily/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/daily/exportDatasets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Export datasets as .json and .csv files for the 3 | # DC Metro Metrics data webpage. 4 | python $OPENSHIFT_REPO_DIR/utils/exportData.py 5 | python $OPENSHIFT_REPO_DIR/utils/exportDB.py 6 | -------------------------------------------------------------------------------- /.openshift/cron/daily/trimLogs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Trim the log files 3 | python $OPENSHIFT_REPO_DIR/utils/trimLogs.py 4 | -------------------------------------------------------------------------------- /.openshift/cron/hourly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.openshift/cron/hourly/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/hourly/exportDB.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Dump MongoDB collections as json files 3 | python $OPENSHIFT_REPO_DIR/utils/exportDB.py 4 | 5 | -------------------------------------------------------------------------------- /.openshift/cron/minutely/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.openshift/cron/minutely/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/minutely/runTwitterApp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # runTwitterApp.py now runs in infinite loop. Commenting out cronjob 4 | #source ~/python-2.7/activate_virtenv 5 | #python $OPENSHIFT_REPO_DIR/scripts/runTwitterApp.py 6 | -------------------------------------------------------------------------------- /.openshift/cron/monthly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/.openshift/cron/monthly/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/weekly/README: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a weekly basis 2 | ===================================== 3 | Any scripts or jobs added to this directory will be run on a scheduled basis 4 | (weekly) using run-parts. 5 | 6 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 7 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} and handles 8 | the files named jobs.deny and jobs.allow specially. 9 | 10 | In this specific example, the chronograph script is the only script or job file 11 | executed on a weekly basis (due to white-listing it in jobs.allow). And the 12 | README and chrono.dat file are ignored either as a result of being black-listed 13 | in jobs.deny or because they are NOT white-listed in the jobs.allow file. 14 | 15 | For more details, please see ../README.cron file. 16 | 17 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/chrono.dat: -------------------------------------------------------------------------------- 1 | Time And Relative D...n In Execution (Open)Shift! 2 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/chronograph: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "`date`: `cat $(dirname \"$0\")/chrono.dat`" 4 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/jobs.allow: -------------------------------------------------------------------------------- 1 | # 2 | # Script or job files listed in here (one entry per line) will be 3 | # executed on a weekly-basis. 4 | # 5 | # Example: The chronograph script will be executed weekly but the README 6 | # and chrono.dat files in this directory will be ignored. 7 | # 8 | # The README file is actually ignored due to the entry in the 9 | # jobs.deny which is checked before jobs.allow (this file). 10 | # 11 | chronograph 12 | 13 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/jobs.deny: -------------------------------------------------------------------------------- 1 | # 2 | # Any script or job files listed in here (one entry per line) will NOT be 3 | # executed (read as ignored by run-parts). 4 | # 5 | 6 | README 7 | 8 | -------------------------------------------------------------------------------- /.openshift/markers/README: -------------------------------------------------------------------------------- 1 | Markers 2 | =========== 3 | 4 | Adding marker files to this directory will have the following effects: 5 | 6 | force_clean_build - Will remove the python virtualenv and force rebuild it 7 | including any previously downloaded libraries 8 | 9 | hot_deploy - Support not yet there - coming soon to a theatre near you. 10 | Will enable the dynamic reloading of python scripts. 11 | 12 | disable_auto_scaling - Will prevent scalable applications from scaling up 13 | or down according to application load. 14 | -------------------------------------------------------------------------------- /ELESAppRunner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | # This runs the ELESApp instance. 4 | 5 | # This module can be executed directly for local testing 6 | """ 7 | 8 | if __name__ == "__main__": 9 | # Local Testing 10 | import test.setup 11 | 12 | # python imports 13 | import os 14 | import sys 15 | import subprocess 16 | from datetime import datetime 17 | import gevent 18 | from gevent import Greenlet 19 | import logging 20 | 21 | # custom imports 22 | from dcmetrometrics.common.globals import DATA_DIR, REPO_DIR, DATA_DIR 23 | from dcmetrometrics.common.restartingGreenlet import RestartingGreenlet 24 | from dcmetrometrics.eles.ELESApp import ELESApp 25 | from dcmetrometrics.keys.keys import MetroEscalatorKeys, MetroElevatorKeys 26 | 27 | OUTPUT_DIR = DATA_DIR 28 | if OUTPUT_DIR is None: 29 | OUTPUT_DIR = os.getcwd() 30 | 31 | if REPO_DIR is None: 32 | SCRIPT_DIR = os.getcwd() 33 | else: 34 | SCRIPT_DIR = os.path.join(REPO_DIR, 'scripts') 35 | 36 | ############################################################### 37 | # Log the ELES App to a file. 38 | LOG_FILE_NAME = os.path.join(DATA_DIR, 'ELESApp.log') 39 | fh = logging.FileHandler(LOG_FILE_NAME) 40 | sh = logging.StreamHandler(sys.stderr) 41 | 42 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 43 | fh.setFormatter(formatter) 44 | sh.setFormatter(formatter) 45 | 46 | logger = logging.getLogger('ELESApp') 47 | logger.addHandler(fh) 48 | logger.addHandler(sh) 49 | ################################################################# 50 | 51 | SLEEP = 30 52 | 53 | ########################################## 54 | # Run the Twitter App as a Greenlet. 55 | class App(RestartingGreenlet): 56 | 57 | def __init__(self, SLEEP=SLEEP, LIVE=False): 58 | RestartingGreenlet.__init__(self, SLEEP=SLEEP, LIVE=LIVE) 59 | self.LIVE = LIVE # Tweet only if Live 60 | self.SLEEP = SLEEP # Sleep time after each tick 61 | self.app = ELESApp(LIVE, MetroEscalatorKeys, MetroElevatorKeys) 62 | 63 | def _run(self): 64 | while True: 65 | try: 66 | self.app.tick() 67 | except Exception as e: 68 | import traceback 69 | logger.error('ElevatorApp caught Exception: %s\n'%(str(e))) 70 | tb = traceback.format_exc() 71 | logger.error('Traceback:\n%s\n\n'%tb) 72 | logger.info("sleeping...") 73 | gevent.sleep(self.SLEEP) 74 | 75 | 76 | if __name__ == "__main__": 77 | print 'Running the ELES app locally....' 78 | a = App() 79 | a.start() 80 | a.join() 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [DC Metro Metrics](http://www.dcmetrometrics.com) is a project dedicated to collecting, analyzing, 2 | and sharing publicly available information about the Washington DC Metrorail system. 3 | 4 | Check out the [Wiki](https://github.com/LeeMendelowitz/DCMetroMetrics/wiki) for information on how to get started. 5 | 6 | -------------------------------------------------------------------------------- /analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/analysis/__init__.py -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This is the main program. 4 | 5 | If you are testing, your environment needs to be properly initialized for this script to work. 6 | For testing: 7 | python test/test_app.py 8 | """ 9 | 10 | import imp 11 | import os 12 | import sys 13 | import subprocess 14 | import argparse 15 | import copy 16 | import gevent 17 | from gevent import monkey; monkey.patch_all() 18 | 19 | 20 | PY_DIR = os.environ['PYTHON_DIR'] 21 | REPO_DIR = os.environ['REPO_DIR'] 22 | SCRIPT_DIR = os.path.join(REPO_DIR, 'scripts') 23 | DATA_DIR = os.environ['DATA_DIR'] 24 | 25 | # Import application modules 26 | from ELESAppRunner import App as ELESAppRunner 27 | 28 | # from elevatorApp import ElevatorApp 29 | from hotCarApp import HotCarApp 30 | 31 | HOT_CAR_TWEET_LIVE = 'HOT_CAR_TWEET_LIVE' in os.environ 32 | ELES_TWEET_LIVE = 'ELES_TWEET_LIVE' in os.environ 33 | 34 | def run(): 35 | 36 | # Run MetroEscalators/MetroElevators twitter App 37 | elesApp = ELESAppRunner(LIVE = ELES_TWEET_LIVE) 38 | elesApp.start() 39 | 40 | # Run HotCar twitter app 41 | hotCarApplication = HotCarApp(LIVE = HOT_CAR_TWEET_LIVE) 42 | hotCarApplication.start() 43 | 44 | gevent.wait() 45 | 46 | if __name__ == '__main__': 47 | run() 48 | -------------------------------------------------------------------------------- /civic.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Metro Metrics", 3 | "description": "DC Metro Metrics is a project dedicated to collecting and sharing publicly available data related to the DC WMATA Metrorail system.", 4 | "license": "GPL-2.0", 5 | "status": "Production", 6 | "type": "Web App", 7 | "homepage": "", 8 | "repository": "https://github.com/LeeMendelowitz/DCMetroMetrics", 9 | "thumbnail": "", 10 | "geography": [ 11 | "Washington, DC" 12 | ], 13 | "contact": { 14 | "name": "Lee Mendelowitz", 15 | "email": "Lee.Mendelowitz@gmail.com", 16 | "url": "https://twitter.com/lmendy7" 17 | }, 18 | "partners": [ 19 | { 20 | "url": "http://codefordc.org", 21 | "name": "Code for DC", 22 | "email": "" 23 | } 24 | ], 25 | "data": [ 26 | { 27 | "url": "http://dcmetrometrics.com/download/dcmetrometrics.zip", 28 | "name": "DCMetroMetrics", 29 | "metadata": "" 30 | } 31 | ], 32 | "tags": [ 33 | "Public Transportation", 34 | "WMATA", 35 | "Open Data" 36 | ], 37 | "links": [ 38 | "http://dcmetrometrics.com/about" 39 | ], 40 | "id": "https://raw.githubusercontent.com/DCgov/civic.json/master/schemas/schema-v1.json" 41 | } -------------------------------------------------------------------------------- /client/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /client/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules* 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | test/* 7 | snapshots/* 8 | -------------------------------------------------------------------------------- /client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | -------------------------------------------------------------------------------- /client/app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/client/app/favicon.ico -------------------------------------------------------------------------------- /client/app/images/capitol_building.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/client/app/images/capitol_building.jpg -------------------------------------------------------------------------------- /client/app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/client/app/images/yeoman.png -------------------------------------------------------------------------------- /client/app/json/units/D11X03ELEVATOR.json: -------------------------------------------------------------------------------- 1 | {"key_statuses": {"lastInspectionStatus": null, "lastStatus": {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000", "tickDelta": 34.702314, "end_time": null}, "currentBreakStatus": null, "lastFixStatus": {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000", "tickDelta": 34.702314, "end_time": null}, "lastOperationalStatus": {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000", "tickDelta": 34.702314, "end_time": null}, "lastBreakStatus": {"symptom_description": "CALLBACK/REPAIR", "symptom_category": "BROKEN", "unit_id": "D11X03ELEVATOR", "metro_open_time": 5980.044, "time": "2014-06-03T22:48:24.859000", "tickDelta": 54.468408, "end_time": "2014-06-04T00:28:04.903000"}}, "statuses": [{"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000+00:00", "tickDelta": 34.702314, "end_time": null}, {"symptom_description": "CALLBACK/REPAIR", "symptom_category": "BROKEN", "unit_id": "D11X03ELEVATOR", "metro_open_time": 5980.044, "time": "2014-06-03T22:48:24.859000+00:00", "tickDelta": 54.468408, "end_time": "2014-06-04T00:28:04.903000+00:00"}, {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": 1.0, "time": "2014-06-03T22:48:23.859000+00:00", "tickDelta": 0.0, "end_time": "2014-06-03T22:48:24.859000+00:00"}], "unit": {"unit_type": "ELEVATOR", "station_code": "D11", "unit_id": "D11X03ELEVATOR", "esc_desc": "Elevator between mezzanine and platform Vienna", "station_desc": "", "station_name": "CHEVERLY", "key_statuses": {"lastInspectionStatus": null, "lastStatus": {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000", "tickDelta": 34.702314, "end_time": null}, "currentBreakStatus": null, "lastFixStatus": {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000", "tickDelta": 34.702314, "end_time": null}, "lastOperationalStatus": {"symptom_description": "OPERATIONAL", "symptom_category": "ON", "unit_id": "D11X03ELEVATOR", "metro_open_time": null, "time": "2014-06-04T00:28:04.903000", "tickDelta": 34.702314, "end_time": null}, "lastBreakStatus": {"symptom_description": "CALLBACK/REPAIR", "symptom_category": "BROKEN", "unit_id": "D11X03ELEVATOR", "metro_open_time": 5980.044, "time": "2014-06-03T22:48:24.859000", "tickDelta": 54.468408, "end_time": "2014-06-04T00:28:04.903000"}}}} -------------------------------------------------------------------------------- /client/app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | 4 | Sitemap: http://www.dcmetrometrics.com/sitemap.xml 5 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/about.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:AboutCtrl 6 | * @description 7 | * # AboutCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('AboutCtrl', ['$scope', 'Page', function ($scope, Page) { 12 | 13 | Page.title('DC Metro Metrics: About'); 14 | Page.description('About the DC Metro Metrics website. What is it? How did it get started? Who can I contact? Where is the source code?'); 15 | 16 | }]); 17 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/datapage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:DatapagCtrl 6 | * @description 7 | * # DatapagCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('DataPageCtrl', ['$scope', 'Page', function ($scope, Page) { 12 | 13 | Page.title('DC Metro Metrics: Data'); 14 | Page.description('Download DC Metro Metrics Data. This includes escalator, elevator, and crowdsourced hot car data about the WMATA Metrorail system in Washington, DC.'); 15 | 16 | }]); 17 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/head.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:HeadCtrl 6 | * @description 7 | * # HeadCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('HeadCtrl', ['$scope', '$rootScope', 'Page', function ($scope, $rootScope, Page) { 12 | $scope.Page = Page; 13 | 14 | // Let's listen to some events on the rootScope for debugging 15 | $rootScope.$on('$viewContentLoaded', function(event, data) { 16 | console.log('Got event! ', event, data); 17 | }); 18 | 19 | $rootScope.$on('viewContentLoaded', function(event, data) { 20 | console.log('Got event! ', event, data); 21 | }); 22 | 23 | }]); 24 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/hotcarpage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:HotcarpageCtrl 6 | * @description 7 | * # HotcarpageCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('HotcarpageCtrl', ['$scope', 'Page', '$stateParams', 'hotCarDirectory', 12 | function ($scope, Page, $stateParams, hotCarDirectory) { 13 | 14 | Page.title("DC Metro Metrics: Hotcar " + $stateParams.carNumber); 15 | Page.description("A listing of WMATA Hotcar reports for car " + $stateParams.carNumber + " crowdsourced from Twitter. Hotcars are " + 16 | " uncomfortably hot rail cars in the WMATA Metrorail system in Washington, DC."); 17 | 18 | $scope.$stateParams = $stateParams; 19 | console.log("have state params: ", $stateParams); 20 | $scope.carNumber = $stateParams.carNumber; 21 | $scope.reports = undefined; 22 | $scope.colors = []; 23 | $scope.colorString = ""; 24 | $scope.loadedTweets = false; 25 | 26 | $scope.postLoad = function() { 27 | $scope.loadedTweets = true; 28 | }; 29 | 30 | hotCarDirectory.get_data().then( function(data) { 31 | 32 | // console.log("got all reports: ", data.allReports); 33 | 34 | // Filter to reports for the given car 35 | // $scope.reports = data.allReports.filter(function(report) { 36 | // return report.car_number == $scope.carNumber; 37 | // }); 38 | 39 | $scope.reports = undefined; 40 | 41 | 42 | if (!data.reportsByCar.hasOwnProperty($scope.carNumber)) { 43 | 44 | // There are no reports for this car. 45 | $scope.reports = []; 46 | $scope.postLoad(); 47 | return; 48 | 49 | } 50 | 51 | $scope.reports = data.reportsByCar[$scope.carNumber]; 52 | 53 | var color2code = { 54 | "RED" : "RD", 55 | "ORANGE" : "OR", 56 | "GREEN" : "GR", 57 | "YELLOW" : "YL", 58 | "BLUE" : "BL", 59 | "SILVER": "SV" 60 | }; 61 | 62 | var co = {}, colorCode, report; 63 | $scope.colors = []; 64 | for(var i = 0; i < $scope.reports.length; i++) { 65 | report = $scope.reports[i]; 66 | if(report.color && !co.hasOwnProperty(report.color)) { 67 | colorCode = color2code[report.color]; 68 | if(colorCode) { 69 | $scope.colors.push(colorCode); 70 | co[report.color] = 1; 71 | } 72 | } 73 | } 74 | 75 | $scope.colorString = $scope.colors.join(); 76 | 77 | }); 78 | 79 | 80 | }]); 81 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:MainCtrl 6 | * @description 7 | * # MainCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('MainCtrl', function ($scope) { 12 | $scope.awesomeThings = [ 13 | 'HTML5 Boilerplate', 14 | 'AngularJS', 15 | 'Karma' 16 | ]; 17 | }); 18 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/mainjumbotron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:MainjumbotronCtrl 6 | * @description 7 | * # MainjumbotronCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('MainJumbotronCtrl', ['$scope', '$location', '$anchorScroll', function ($scope, $location, $anchorScroll) { 12 | 13 | $scope.$location = $location; 14 | $scope.$anchorScroll = $anchorScroll; 15 | 16 | $scope.goToOutageCalendar = function() { 17 | 18 | // set the location.hash to the id of 19 | // the element you wish to scroll to. 20 | $location.hash('dcmm-escalator-outage-calendar'); 21 | 22 | // call $anchorScroll() 23 | $anchorScroll(); 24 | }; 25 | 26 | }]); 27 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/nav.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:NavCtrl 6 | * @description 7 | * # NavCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | 12 | .controller('NavCtrl', function ($scope) { 13 | 14 | $scope.navbarCollapsed = true; 15 | 16 | $scope.toggleNav = function() { 17 | $scope.navbarCollapsed = !$scope.navbarCollapsed; 18 | }; 19 | 20 | $scope.collapseNav = function() { 21 | $scope.navbarCollapsed = true; 22 | }; 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/sitemap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:SitemapCtrl 6 | * @description 7 | * # SitemapCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('SitemapCtrl', ["$scope", "$state", "directory", "hotCarDirectory", function ($scope, $state, 12 | directory, hotCarDirectory) { 13 | 14 | $scope.$state = $state; 15 | 16 | 17 | //Get Escalator data 18 | directory.get_directory().then(function(data) { 19 | $scope.directory = data; 20 | $scope.station_names = []; 21 | $scope.station_short_names = []; 22 | var station; 23 | for(var k in $scope.directory.directory) { 24 | station = $scope.directory.directory[k]; 25 | $scope.station_names.push(k); 26 | $scope.station_short_names.push(station.short_name); 27 | } 28 | }); 29 | 30 | directory.get_unit_dict().then(function(data) { 31 | $scope.unit_dict = data; 32 | 33 | $scope.unit_ids = []; 34 | for(var k in $scope.unit_dict) { 35 | $scope.unit_ids.push(k); 36 | } 37 | 38 | }); 39 | 40 | // Get Hotcar data 41 | 42 | hotCarDirectory.get_data().then(function(data) { 43 | $scope.hot_car_data = data; 44 | $scope.hot_car_numbers = []; 45 | for(var k in data.reportsByCar) { 46 | $scope.hot_car_numbers.push(k); 47 | } 48 | }); 49 | 50 | 51 | }]); 52 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/stationdirectory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:StationdirectoryCtrl 6 | * @description 7 | * # StationdirectoryCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('StationDirectoryCtrl', ['$scope', 'Page', 'directory', function ($scope, Page, directory) { 12 | 13 | Page.title('DC Metro Metrics: Station Listing'); 14 | Page.description('List of all stations in the in the WMATA Metrorail system in Washington, DC.'); 15 | 16 | // Request station directory data 17 | directory.get_directory().then( function(data) { 18 | $scope.stationDirectory = data.directory; 19 | 20 | }); 21 | 22 | 23 | 24 | }]); 25 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/statusentry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:StatusentryCtrl 6 | * @description 7 | * # StatusentryCtrl 8 | * Controller of the dcmetrometricsApp 9 | * Extract some useful things from a status. 10 | */ 11 | angular.module('dcmetrometricsApp') 12 | .controller('StatusentryCtrl', ['$scope', 'directory', function ($scope, directory) { 13 | 14 | $scope.directory = directory; 15 | 16 | // Get the unit from the status for the current scope. 17 | $scope.unit = undefined; 18 | $scope.stationLines = undefined; 19 | 20 | 21 | // We can simply assume that a controller has initialized 22 | // the directory and recent_upates data before this controller 23 | // is initialized. 24 | 25 | // directory.get_directory().then(function(data) { 26 | // directory.get_recent_updates().then(function(data) { 27 | 28 | $scope.unit = directory.unitFromStatus($scope.status); 29 | $scope.stationLines = directory.getStationLinesForStatus($scope.status); 30 | 31 | // }); 32 | // }); 33 | 34 | }]); 35 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/unitentry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:UnitentryCtrl 6 | * @description 7 | * # UnitentryCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('UnitentryCtrl', ['$scope', 'directory', function ($scope, directory) { 12 | 13 | // Get the unit from the status for the current scope. 14 | $scope.stationLines = directory.getStationLines($scope.unit); 15 | 16 | }]); 17 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/unitpage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:UnitCtrl 6 | * @description 7 | * # UnitCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('UnitPageCtrl', ['$scope', 'Page', '$stateParams', '$filter', 'unitService', 'directory', 'statusTableUtils', 'UnitStatus', 12 | function ($scope, Page, $stateParams, $filter, unitService, directory, statusTableUtils, UnitStatus) { 13 | 14 | 15 | Page.title("DC Metro Metrics: " + $filter('unitIdToHuman')($stateParams.unitId)); 16 | Page.description(" "); 17 | 18 | 19 | $scope.unitId = $stateParams.unitId; 20 | $scope.statusTableUtils = statusTableUtils; 21 | 22 | 23 | 24 | unitService.getUnitData($scope.unitId).then( function(data) { 25 | 26 | // console.log("have unit data: ", data); 27 | $scope.unitData = data; 28 | $scope.unit = $scope.unitData; 29 | $scope.key_statuses = $scope.unitData.key_statuses; 30 | var stationCode = data.station_code; 31 | 32 | // Get the station data for this unit. 33 | directory.get_directory().then(function(stationDirectory) { 34 | // console.log("Have directory data: ", stationDirectory); 35 | $scope.stationData = stationDirectory.codeToData[stationCode]; 36 | Page.description("Performance history for " + $filter('unitIdToHuman')($scope.unitId) + " at " + $scope.stationData.long_name + " station in the WMATA Metrorail system in Washington, DC."); 37 | 38 | }); 39 | 40 | 41 | }); 42 | 43 | 44 | 45 | $scope.showSummary = function() { 46 | return $scope.$state.is("unit") || 47 | $scope.$state.is("unit.summary"); 48 | }; 49 | 50 | $scope.showStatuses = function() { 51 | return $scope.$state.is("unit.statuses"); 52 | }; 53 | 54 | $scope.showCalendar = function() { 55 | return $scope.$state.is("unit.calendar"); 56 | }; 57 | 58 | 59 | }]); 60 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/unitperformanceaccordian.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:UnitperformanceaccordianCtrl 6 | * @description 7 | * # UnitperformanceaccordianCtrl 8 | * Controller of the dcmetrometricsApp 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .controller('UnitPerformanceAccordianCtrl', function ($scope) { 12 | 13 | $scope.accordianItems = []; 14 | $scope.oneAtATime = false; 15 | $scope.dataKeyList = []; // List of Row Names 16 | 17 | var keys = ["all_time", "one_day", "three_day", "seven_day", "fourteen_day", "thirty_day"]; 18 | $scope.keys = keys; 19 | var headings = ["All Time", "1 Day", "3 Day", "7 Day", "14 Day", "30 Day"]; // Column Names 20 | $scope.headings = headings; 21 | 22 | $scope.$watch("unitData", function(newVal, oldVal) { 23 | 24 | $scope.accordianItems = []; 25 | 26 | var performanceSummary = $scope.unitData && $scope.unitData.performance_summary; 27 | 28 | if (performanceSummary === undefined) { 29 | return; 30 | } 31 | 32 | var i, dk, key, heading, summary, isOpen; 33 | 34 | var dataKeys = {}; 35 | 36 | for(i = 0; i < keys.length; i++) { 37 | 38 | key = keys[i]; 39 | heading = headings[i]; 40 | summary = performanceSummary[key]; 41 | 42 | isOpen = i === 0; 43 | 44 | if( summary === undefined) { 45 | continue; 46 | } 47 | 48 | $scope.accordianItems.push( { 49 | heading: heading, 50 | summary: summary, 51 | isOpen: isOpen 52 | }); 53 | 54 | for(dk in summary) { 55 | if (summary.hasOwnProperty(dk)) { 56 | dataKeys[dk] = 1; 57 | } 58 | } 59 | } 60 | 61 | $scope.dataKeyList = []; 62 | for(dk in dataKeys) { 63 | if(dataKeys.hasOwnProperty(dk)) { 64 | $scope.dataKeyList.push(dk); 65 | } 66 | } 67 | 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/unitstatusutils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name dcmetrometricsApp.controller:UnitstatusutilsCtrl 6 | * @description 7 | * # UnitstatusutilsCtrl 8 | * Controller of the dcmetrometricsApp. It adds some useful 9 | * methods to the scope for working with unit and status things. 10 | */ 11 | angular.module('dcmetrometricsApp') 12 | .controller('UnitstatusutilsCtrl', ['$scope', 'directory', function ($scope, directory) { 13 | 14 | // Expose methods from the directory service 15 | $scope.utils = directory; 16 | 17 | }]); 18 | -------------------------------------------------------------------------------- /client/app/scripts/directives/affix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:affix 6 | * @description 7 | * # affix 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('affix', function () { 11 | return { 12 | restrict: 'A', 13 | link: function postLink(scope, element, attrs) { 14 | element.affix({ 15 | offset: { 16 | top: scope.offsetTop 17 | } 18 | }); 19 | }, 20 | scope: { 21 | offsetTop: "@" 22 | } 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /client/app/scripts/directives/dailyservicereportunitstatustable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:dailyServiceReportUnitStatusTable 6 | * @description 7 | * # dailyServiceReportUnitStatusTable 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('dailyservicereportunitstatustable', function () { 11 | return { 12 | templateUrl: "/views/dailyservicereportunitstatustable.html", 13 | restrict: 'E', 14 | scope: { 15 | statuses: '=' 16 | } 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /client/app/scripts/directives/linecolors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:lineColors 6 | * @description 7 | * # lineColors 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('linecolors', function () { 11 | return { 12 | template: '
', 13 | restrict: 'E', 14 | link: function postLink(scope, element, attrs) { 15 | 16 | // Process the list of line codes, converting the string to 17 | // a list 18 | 19 | scope.lineList = []; 20 | 21 | scope.$watch("lines", function(lines){ 22 | scope.lineList = []; 23 | if (scope.lines) { scope.lineList = scope.lines.replace(/ /g, '').split(","); } 24 | }); 25 | 26 | 27 | //// COMMENT 28 | // var $e = $(element); 29 | 30 | // scope.$watch("lines", function(lines) { 31 | // // console.log("link watch sees lines: ", lines); 32 | // var lines = scope.lines && scope.lines.replace(/ /g, '').split(","); 33 | // $e.html(""); 34 | // if (!lines) return; 35 | // for(var i = 0; i < lines.length; i++) { 36 | // $e.append($('
')); 37 | // } 38 | // }); 39 | //// END COMMENT 40 | 41 | // console.log("link outside watch sees scope.lines: ", scope.lines); 42 | }, 43 | scope: { 44 | lines: '@' 45 | }, 46 | replace: true 47 | }; 48 | }); 49 | -------------------------------------------------------------------------------- /client/app/scripts/directives/scrolltarget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:scrollTarget 6 | * @description 7 | * # scrollTarget 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('scrollTarget', function () { 11 | return { 12 | restrict: 'A', 13 | link: function postLink(scope, element, attrs) { 14 | var id = attrs.id; 15 | console.log("scope, elment, attrs: ", scope, element, attrs); 16 | scope.registerScrollTarget(id, element); 17 | }, 18 | require: '^scrollTargetCollector' 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /client/app/scripts/directives/scrolltargetcollector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:scrollTargetCollector 6 | * @description 7 | * # scrollTargetCollector 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('scrollTargetCollector', function () { 11 | return { 12 | restrict: 'A', 13 | link: function postLink(scope, element, attrs) { 14 | }, 15 | controller: ['$scope', '$uiViewScroll', '$timeout', function($scope, $uiViewScroll, $timeout) { 16 | 17 | $scope.watchTargets = {}; 18 | 19 | var BODY_OFFSET = 40; // pixels from body top. This should be set somewhere as a global. 20 | 21 | $scope.curScrollTarget = ""; 22 | 23 | $scope.registerScrollTarget = function(id, elem) { 24 | id && elem && ($scope.watchTargets[id] = elem); 25 | }; 26 | 27 | $scope.scrollTo = function(id) { 28 | 29 | var elem = $scope.watchTargets[id]; 30 | if (!elem) { return; } 31 | 32 | var offsetTop = elem.offset().top; 33 | 34 | $timeout( function() { 35 | $scope.curScrollTarget = id; 36 | $(document).scrollTop(offsetTop - BODY_OFFSET); 37 | }); 38 | 39 | 40 | 41 | }; 42 | 43 | }] 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /client/app/scripts/directives/scrolltobookmark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:scrollToBookmark 6 | * @description 7 | * # scrollToBookmark 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('scrollToBookmark', function () { 11 | 12 | return { 13 | restrict: 'A', 14 | link: function(scope, element, attrs) { 15 | var value = attrs.scrollToBookmark; 16 | element.click(function() { 17 | scope.$apply(function() { 18 | var selector = "[scroll-bookmark='"+ value +"']"; 19 | var element = $(selector); 20 | if(element.length) { 21 | window.scrollTo(0, element[0].offsetTop - 100); // Don't want the top to be the exact element, -100 will go to the top for a little bit more 22 | } 23 | }); 24 | }); 25 | } 26 | }; 27 | 28 | }); 29 | 30 | 31 | -------------------------------------------------------------------------------- /client/app/scripts/directives/tweetline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc directive 5 | * @name dcmetrometricsApp.directive:tweetline 6 | * @description 7 | * # tweetline 8 | */ 9 | angular.module('dcmetrometricsApp') 10 | .directive('tweetline', ['$compile', '$timeout', 'usSpinnerService', function ($compile, $timeout, usSpinnerService) { 11 | 12 | return { 13 | 14 | templateUrl: '/views/tweetlinepartial.html', 15 | restrict: 'E', 16 | link: function postLink(scope, element, attrs) { 17 | 18 | scope.$watch('reports', function(reports, reportsOld) { 19 | 20 | if (reports === undefined) { return; } 21 | 22 | if( (reports instanceof Array) && reports.length === 0) { 23 | usSpinnerService.stop('tweet-spinner'); 24 | } 25 | 26 | scope.$evalAsync(function() { 27 | scope.renderTweets(); 28 | }); 29 | 30 | }); 31 | 32 | // Create a timeout to show the tweets. This is done to avoid 33 | // the case where twttr widgets takes to long to load or do its stuff. 34 | scope.delayedShow = $timeout(function() { 35 | console.log("doing a delayed show because twttr widgets took too long."); 36 | scope.showTweets(); 37 | }, 2000); 38 | }, 39 | controller: ['$scope', function($scope) { 40 | 41 | $scope.renderedTweets = false; 42 | 43 | $scope.showTweets = function() { 44 | $scope.$apply( function() { 45 | $scope.renderedTweets = true; 46 | usSpinnerService.stop('tweet-spinner'); 47 | $scope.postLoad(); 48 | }); 49 | 50 | // Cancel the timeout 51 | $timeout.cancel($scope.delayedShow); 52 | 53 | }; 54 | 55 | $scope.renderTweets = function() { 56 | 57 | // Use the Twitter widgets.js to style the tweets 58 | twttr.events && twttr.events._handlers && twttr.events.unbind && twttr.events.unbind('loaded'); 59 | twttr.events && twttr.events.bind && twttr.events.bind('loaded', function (event) { 60 | $scope.showTweets(); 61 | }); 62 | 63 | twttr.widgets && twttr.widgets.load(); 64 | 65 | }; 66 | 67 | var color2code = { 68 | "RED" : "RD", 69 | "ORANGE" : "OR", 70 | "GREEN" : "GR", 71 | "YELLOW" : "YL", 72 | "BLUE" : "BL", 73 | "SILVER": "SV" 74 | }; 75 | 76 | $scope.colorStringFromReport = function(report) { 77 | var colorString = ''; 78 | if (report.color && color2code.hasOwnProperty(report.color)) { 79 | colorString += color2code[report.color]; 80 | } 81 | return colorString; 82 | }; 83 | 84 | 85 | }], 86 | scope: { 87 | reports: '=', 88 | postLoad: '&', 89 | showLink: '=' 90 | } 91 | }; 92 | }]); -------------------------------------------------------------------------------- /client/app/scripts/directives/voronoi_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /client/app/scripts/filters/capfirst.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc filter 5 | * @name dcmetrometricsApp.filter:capFirst 6 | * @function 7 | * @description 8 | * # capFirst 9 | * Filter in the dcmetrometricsApp. 10 | */ 11 | angular.module('dcmetrometricsApp') 12 | .filter('capFirst', function () { 13 | return function (input) { 14 | 15 | // Capitalize the first letter of 16 | // each element in array. Return as list. 17 | var capFirst = function(a) { 18 | return a.map(function(word) { 19 | return word[0].toUpperCase() + word.slice(1).toLowerCase(); 20 | }); 21 | }; 22 | 23 | var ret = input.split(" "); 24 | ret = ret.map(function(e) { 25 | var a = e.split("/"); 26 | return capFirst(a); 27 | }); 28 | 29 | // Join on "/" 30 | ret = ret.map(function(e) { return e.join("/");}); 31 | ret = ret.join(" "); 32 | return ret; 33 | 34 | }; 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /client/app/scripts/filters/duration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc filter 5 | * @name dcmetrometricsApp.filter:duration 6 | * @function 7 | * @description 8 | * # duration 9 | * Filter in the dcmetrometricsApp. 10 | */ 11 | angular.module('dcmetrometricsApp') 12 | .filter('duration', function () { 13 | return function (input) { 14 | 15 | // Input is in milliseconds. Convert to seconds. 16 | var seconds = parseInt(input)/1000.0; 17 | var days = 0, hours = 0, minutes = 0; 18 | var remainder = seconds; 19 | 20 | if (remainder >= 3600*24) { 21 | days = Math.floor(remainder/(3600 * 24)); 22 | remainder = remainder - days*3600*24; 23 | } 24 | 25 | if (remainder >= 3600) { 26 | hours = Math.floor(remainder/3600.0); 27 | remainder = remainder - hours*3600; 28 | } 29 | 30 | if (remainder >= 60) { 31 | minutes = Math.floor(remainder/60.0); 32 | remainder = remainder - minutes*60; 33 | } 34 | 35 | var ret = ""; 36 | if (days) { 37 | ret = ret + days + "d"; 38 | } 39 | if (hours) { 40 | ret = ret + " " + hours + "h"; 41 | } 42 | 43 | ret = ret + " " + minutes + "m"; 44 | 45 | return ret; 46 | 47 | }; 48 | }); 49 | -------------------------------------------------------------------------------- /client/app/scripts/filters/percentage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc filter 5 | * @name dcmetrometricsApp.filter:percentage 6 | * @function 7 | * @description 8 | * # percentage 9 | * Filter in the dcmetrometricsApp. 10 | */ 11 | 12 | angular.module('dcmetrometricsApp') 13 | .filter('percentage', ['$filter', function ($filter) { 14 | return function (input, decimals) { 15 | return $filter('number')(input * 100, decimals) + '%'; 16 | }; 17 | }]); -------------------------------------------------------------------------------- /client/app/scripts/filters/unitidtohuman.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc filter 5 | * @name dcmetrometricsApp.filter:unitIdToHuman 6 | * @function 7 | * @description 8 | * # unitIdToHuman 9 | * Filter in the dcmetrometricsApp. 10 | */ 11 | angular.module('dcmetrometricsApp') 12 | .filter('unitIdToHuman', function () { 13 | return function (input) { 14 | var code = input.substring(0,6); 15 | var unit_type = input.substring(6); 16 | unit_type = unit_type.charAt(0).toUpperCase() + unit_type.substring(1).toLowerCase(); 17 | return unit_type + " " + code; 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/scripts/services/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name dcmetrometricsApp.Unit 6 | * @description 7 | * # Unit 8 | * Factory in the dcmetrometricsApp. 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .factory('Page', function (UnitStatus) { 12 | 13 | var title = "DC Metro Metrics"; 14 | var description = ""; 15 | var h; 16 | return { 17 | 18 | title: function(s) { 19 | title = s || title; 20 | return title; 21 | }, 22 | setTitle: function(s) { 23 | title = s; 24 | }, 25 | description: function(s) { 26 | description = s || description; 27 | return description; 28 | }, 29 | setDescription: function(s) { 30 | description = s; 31 | }, 32 | navbarHeight: function() { 33 | var ret = h || (h = $('.dcmm-navbar').height()); // compute once and cache. 34 | return ret; 35 | } 36 | 37 | }; 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /client/app/scripts/services/stationdata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name dcmetrometricsApp.stationData 6 | * @description 7 | * # StationData 8 | * Factory in the dcmetrometricsApp. 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .factory('StationData', ['Unit', 'UnitStatus', function (Unit, UnitStatus) { 12 | // Service logic 13 | // ... 14 | 15 | var StationData = function(data) { 16 | 17 | this.stations = data.stations; 18 | this.escalators = data.escalators.map(function(d) { return new Unit(d); }); 19 | this.elevators = data.elevators.map(function(d) { return new Unit(d); }); 20 | this.recent_statuses = data.recent_statuses.map(function(d) { return new UnitStatus(d); }); 21 | 22 | this.long_name = data.stations[0].long_name; 23 | this.short_name = data.stations[0].short_name; 24 | this.all_codes = data.stations[0].all_codes; 25 | this.all_lines = data.stations[0].all_lines; 26 | 27 | }; 28 | 29 | 30 | 31 | 32 | StationData.prototype = { 33 | 34 | }; 35 | 36 | // Public API here 37 | return StationData; 38 | 39 | }]); 40 | -------------------------------------------------------------------------------- /client/app/scripts/services/statustableutils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name dcmetrometricsApp.statusTableUtils 6 | * @description 7 | * # statusTableUtils 8 | * Service in the dcmetrometricsApp. 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .service('statusTableUtils', function statusTableUtils() { 12 | // AngularJS will instantiate a singleton by calling "new" on this function 13 | 14 | // Status category to table class 15 | var catToClass = { 16 | BROKEN : 'danger', 17 | INSPECTION : 'warning', 18 | OFF : 'warning', 19 | ON : 'success', 20 | REHAB : 'info' 21 | }; 22 | 23 | 24 | this.getRowClass = function(status) { 25 | var category = status.symptom_category; 26 | return catToClass[category]; 27 | }; 28 | 29 | this.getDuration = function(status) { 30 | var end_time = status.end_time ? new Date(status.end_time) : new Date(); 31 | var start_time = new Date(status.time); 32 | var duration = end_time - start_time; 33 | return duration; 34 | }; 35 | 36 | this.getTimeSince = function(status) { 37 | var start_time = new Date(status.time); 38 | var timeSince = (new Date()) - start_time; 39 | return timeSince; 40 | }; 41 | 42 | 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /client/app/scripts/services/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name dcmetrometricsApp.Unit 6 | * @description 7 | * # Unit 8 | * Factory in the dcmetrometricsApp. 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .factory('Unit', ['UnitStatus', function (UnitStatus) { 12 | 13 | // Service logic 14 | 15 | var convertKeyStatuses = function(ks) { 16 | var k, s; 17 | for(k in ks) { 18 | if (ks.hasOwnProperty(k)) { 19 | s = ks[k]; 20 | if (s) { 21 | ks[k] = new UnitStatus(s); 22 | } 23 | } 24 | } 25 | return ks; 26 | }; 27 | 28 | var Unit = function(data) { 29 | 30 | this.esc_desc = data.esc_desc; 31 | this.key_statuses = convertKeyStatuses(data.key_statuses); 32 | this.performance_summary = data.performance_summary; 33 | this.station_code = data.station_code; 34 | this.station_desc = data.station_desc; 35 | this.station_name = data.station_name; 36 | this.unit_id = data.unit_id; 37 | this.unit_type = data.unit_type; 38 | 39 | // Note: The statuses may not be set. Depends on whether the data was requested 40 | // for a unit's page. The station directory does not include all of the unit statuses. 41 | this.statuses = data.statuses || []; 42 | this.statuses = this.statuses.map(function(d) { return new UnitStatus(d); }); 43 | 44 | }; 45 | 46 | 47 | Unit.prototype = { 48 | isEscalator: function() { 49 | return this.unit_type === "ESCALATOR"; 50 | }, 51 | isElevator: function() { 52 | return this.unit_type === "ELEVATOR"; 53 | } 54 | }; 55 | 56 | // Public API here 57 | return Unit; 58 | 59 | }]); 60 | -------------------------------------------------------------------------------- /client/app/scripts/services/unitstatus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name dcmetrometricsApp.unitStatus 6 | * @description 7 | * # unitStatus 8 | * Factory in the dcmetrometricsApp. 9 | */ 10 | angular.module('dcmetrometricsApp') 11 | .factory('UnitStatus', ['$filter', function ($filter) { 12 | 13 | // Service logic 14 | // ... 15 | var zone = "America/New_York"; 16 | 17 | var UnitStatus = function(data) { 18 | 19 | this.unit_id = data.unit_id; 20 | this.time = data.time; 21 | this.is_active = (this.end_time == null); 22 | this.actual_end_time = data.end_time; 23 | this.end_time = data.end_time || moment(); // Hack to make end_time now. 24 | this.metro_open_time = data.metro_open_time; 25 | this.update_type = data.update_type; 26 | this.symptom_description = data.symptom_description; 27 | this.symptom_category = data.symptom_category; 28 | 29 | // Convert times to moments with the East coast time zone 30 | this.end_time = moment.tz(this.end_time, zone); 31 | this.actual_end_time = this.actual_end_time && moment.tz(this.actual_end_time, zone); 32 | this.time = moment.tz(this.time, zone); 33 | this.start_time = this.time; 34 | this.duration = this.end_time - this.start_time; 35 | 36 | }; 37 | 38 | UnitStatus.prototype = { 39 | 40 | // Get Days that overlap the status 41 | getDays : function() { 42 | 43 | var ret = []; 44 | 45 | var start_day = this.time.clone().startOf('day'); 46 | var end_day = this.end_time.clone().startOf('day'); 47 | var day = start_day; 48 | while(day <= end_day) { 49 | ret.push(day.clone()); 50 | day.add(1, 'd'); 51 | } 52 | 53 | return ret; 54 | 55 | }, 56 | isBroken : function() { 57 | return this.symptom_category === "BROKEN"; 58 | }, 59 | isInspection: function() { 60 | return this.symptom_category === "INSPECTION"; 61 | }, 62 | isOff: function() { 63 | return this.symptom_category === "OFF"; 64 | }, 65 | isOperational: function() { 66 | return this.symptom_category === "ON"; 67 | } 68 | 69 | 70 | }; 71 | 72 | // Public API here 73 | return UnitStatus; 74 | 75 | }]); 76 | -------------------------------------------------------------------------------- /client/app/styles/from_angular_strap.css: -------------------------------------------------------------------------------- 1 | @media (max-width:991px) { 2 | .bs-sidebar.affix { 3 | position:static!important 4 | } 5 | } 6 | 7 | .bs-sidenav { 8 | margin-top:40px; 9 | margin-bottom:20px; 10 | width:124px 11 | } 12 | 13 | .bs-sidebar .nav>li>a { 14 | display:block; 15 | font-size:13px; 16 | font-weight:500; 17 | color:#999; 18 | padding:4px 15px; 19 | text-align:right 20 | } 21 | 22 | .bs-sidebar .nav>li>a:focus,.bs-sidebar .nav>li>a:hover { 23 | padding-right:14px; 24 | color:#b94846; 25 | text-decoration:none; 26 | background-color:transparent; 27 | border-right:1px solid #b94846 28 | } 29 | 30 | .bs-sidebar .nav>.active:focus>a,.bs-sidebar .nav>.active:hover>a,.bs-sidebar .nav>.active>a { 31 | padding-right:13px; 32 | font-weight:700; 33 | color:#b94846; 34 | background-color:transparent; 35 | border-right:2px solid #b94846 36 | } 37 | 38 | .bs-sidebar .nav .nav { 39 | display:none; 40 | padding-bottom:10px 41 | } 42 | 43 | .bs-sidebar .nav .nav>li>a { 44 | padding-top:2px; 45 | padding-bottom:2px; 46 | padding-right:30px; 47 | font-size:12px; 48 | font-weight:400 49 | } 50 | 51 | .bs-sidebar .nav .nav>li>a:focus,.bs-sidebar .nav .nav>li>a:hover { 52 | padding-right:29px 53 | } 54 | 55 | .bs-sidebar .nav .nav>.active:focus>a,.bs-sidebar .nav .nav>.active:hover>a,.bs-sidebar .nav .nav>.active>a { 56 | font-weight:500; 57 | padding-right:28px 58 | } 59 | 60 | @media (min-width:992px) { 61 | .bs-sidebar .nav>.active>ul { 62 | display:block 63 | } 64 | 65 | .bs-sidebar.affix .bs-sidenav,.bs-sidebar.affix-bottom .bs-sidenav { 66 | margin-top:0; 67 | margin-bottom:0 68 | } 69 | } 70 | 71 | @media (min-width:1200px) { 72 | .bs-sidebar { 73 | margin-right:30px 74 | } 75 | } -------------------------------------------------------------------------------- /client/app/styles/github_fork.css: -------------------------------------------------------------------------------- 1 | /* Adapted from: http://codepo8.github.io/css-fork-on-github-ribbon/ */ 2 | 3 | #forkongithub a { 4 | background:#333333; 5 | color:#fff; 6 | text-decoration:none; 7 | font-family:arial,sans-serif; 8 | text-align:center; 9 | font-weight:700; 10 | padding:5px 40px; 11 | font-size:1.5rem; 12 | line-height:2rem; 13 | box-sizing: content-box; 14 | position:relative; 15 | transition:.5s; 16 | display: none; 17 | } 18 | 19 | #forkongithub a:hover { 20 | background:#00539e; 21 | color:#fff 22 | } 23 | 24 | #forkongithub a::before,#forkongithub a::after { 25 | content:""; 26 | width:100%; 27 | display:block; 28 | position:absolute; 29 | top:1px; 30 | left:0; 31 | height:1px; 32 | background:#fff 33 | } 34 | 35 | #forkongithub a::after { 36 | bottom:1px; 37 | top:auto 38 | } 39 | 40 | @media screen and (min-width:768px) { 41 | #forkongithub { 42 | position:fixed; 43 | display:block; 44 | top:0; 45 | right:0; 46 | width:200px; 47 | overflow:hidden; 48 | height:200px; 49 | z-index:9999 50 | } 51 | 52 | #forkongithub a { 53 | width:200px; 54 | position:absolute; 55 | top:60px; 56 | right:-60px; 57 | transform:rotate(45deg); 58 | -webkit-transform:rotate(45deg); 59 | -ms-transform:rotate(45deg); 60 | -moz-transform:rotate(45deg); 61 | -o-transform:rotate(45deg); 62 | box-shadow:4px 4px 10px rgba(0,0,0,0.8); 63 | display: block; 64 | } 65 | } -------------------------------------------------------------------------------- /client/app/styles/table.css: -------------------------------------------------------------------------------- 1 | th.tablesort-sortable { 2 | -webkit-user-select: none; 3 | -khtml-user-select: none; 4 | -moz-user-select: none; 5 | -o-user-select: none; 6 | user-select: none; 7 | cursor: pointer; 8 | } 9 | 10 | th.tablesort-sortable:hover { 11 | background-color:rgba(141, 192, 219, 0.25); 12 | } 13 | 14 | table .tablesort-sortable:after{ 15 | content:""; 16 | float:right; 17 | margin-top:7px; 18 | visibility:hidden; 19 | border-left:4px solid transparent; 20 | border-right:4px solid transparent; 21 | 22 | border-top:none; 23 | border-bottom:4px solid #000; 24 | } 25 | 26 | table .tablesort-desc:after{ 27 | border-top:4px solid #000; 28 | border-bottom:none; 29 | } 30 | 31 | table .tablesort-asc,table .tablesort-desc{ 32 | background-color:rgba(141, 192, 219, 0.25); 33 | } 34 | 35 | table .tablesort-sortable:hover:after, table .tablesort-asc:after, table .tablesort-desc:after { 36 | visibility:visible; 37 | } 38 | 39 | /* 40 | * Styling for the table row shown in empty tables 41 | */ 42 | 43 | /* The row is always added as the first row in a table 44 | Hide it by default */ 45 | .showIfLast { 46 | display: none; 47 | } 48 | 49 | /* Only show it if it is also the last row of the table. */ 50 | .showIfLast:last-child { 51 | display: table-row; 52 | } 53 | 54 | .showIfLast td { 55 | text-align: center; 56 | } 57 | 58 | .showIfLast td:after { 59 | content: "No data"; 60 | } -------------------------------------------------------------------------------- /client/app/views/dailyservicereportunitstatustable.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
ItemUnitDescriptionStatusTypeStartEndDuration
{{ $index }} 22 | {{ status.unit_id | unitIdToHuman }} 23 | 26 | 27 | 28 | {{ directory.getStationName(directory.unitFromStatus(status)) }} 29 | 30 | 31 |
32 | {{ directory.unitDescription(unit) }} 33 | 34 |
{{ status.symptom_description | capFirst }}{{ status.update_type }}{{ status.time.format("MM/DD/YY h:mm A") }}{{ status.actual_end_time.format("MM/DD/YY h:m A") }}{{ status.duration | duration }}
44 |
-------------------------------------------------------------------------------- /client/app/views/data.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Data

5 |
6 |
7 | 8 | 9 |
10 | 11 |

From this page you can download all of the data used to 12 | generate this website. All files are updated daily.

13 | 14 |

License

15 | 16 |

17 | All DC Metro Metrics data is made available as open data 18 | under the 19 | 20 | Open Database License (ODbL) v1.0. 21 |

22 | 23 |

In summary, you are free to:

24 | 25 | 30 | 31 | 32 |

provided that:

33 | 34 | 35 | 40 | 41 | 42 |

Please see the license for full details.

43 | 44 | 45 |

Download

46 | 47 | 48 |

Click Here to download all of the DC Metro Metrics data. This zip file is regenerated once per day.

49 | 50 |

For information on the meaning of the columns, 51 | check out the README.md included in the zip file. Please note that all timestamps are in the ISO 8601 format in UTC. 52 |

53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /client/app/views/hotcarpage.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hotcar {{ carNumber }}

4 | 5 | 6 |

Reported {{ reports.length }} times on

7 | 8 |
9 |
10 | 11 |
12 | 13 | 14 |

Tweets

15 | 16 |
17 | 18 |
19 | 20 |
-------------------------------------------------------------------------------- /client/app/views/press.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Press

4 |

5 | featuring DC Metro Metrics, 6 | @MetroEscalators, 7 | @MetroElevators, 8 | and 9 | @MetroHotCars. 10 |

11 |
12 |
13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |

Here is a list of articles, blog posts, radio stories, and TV stories that have 32 | featured data from DC Metro Metrics:

33 | 34 | 43 | 44 | 45 |
46 |
47 | 48 | 49 |
-------------------------------------------------------------------------------- /client/app/views/sitemap.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | {{ $state.href("home", {}, {absolute: true})}}
7 | {{ $state.href("stations.list", {}, {absolute: true})}}
8 | {{ $state.href("about", {}, {absolute: true})}}
9 | {{ $state.href("outages", {}, {absolute: true})}}
10 | {{ $state.href("rankings", {}, {absolute: true})}}
11 | 12 | 13 | {{$state.href("stations.detail", {station: short_name}, {absolute: true}) }}
14 |
15 | 16 | 17 | {{$state.href("unit", { unitId: unit_id }, {absolute: true}) }}
18 |
19 | 20 | 21 | 22 | {{$state.href("hotcars.detail", { carNumber: car_number }, {absolute: true} ) }}
23 |
24 | 25 |
26 | 27 | 28 |
-------------------------------------------------------------------------------- /client/app/views/stationlisting.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Station Directory

4 |
5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 |
StationEscalatorsElevators
{{ station.stations[0].long_name }} 18 | {{ station.escalators.length }}{{ station.elevators.length }}
24 | 25 |
-------------------------------------------------------------------------------- /client/app/views/tweetlinepartial.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

There are no reports for this car.

6 | 7 |
8 |
10 | 11 |
12 |
13 | 14 | Car {{ report.car_number }} 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 | 33 |
-------------------------------------------------------------------------------- /client/app/views/unit_calendar_heatmap_partial.html: -------------------------------------------------------------------------------- 1 |
2 |

{{header}}

3 |

{{description}}

4 | 7 | 10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /client/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcmetrometrics", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "angular": "1.3.13", 6 | "json3": "~3.3.1", 7 | "es5-shim": "~3.1.0", 8 | "bootstrap": "~3.2.0", 9 | "angular-resource": "1.3.13", 10 | "angular-cookies": "1.3.13", 11 | "angular-sanitize": "1.3.13", 12 | "angular-animate": "1.3.13", 13 | "angular-touch": "1.3.13", 14 | "angular-route": "1.3.13", 15 | "angular-strap": "2.2.0", 16 | "angular-bootstrap": "~0.11.0", 17 | "angular-ui-router": "~0.2.10", 18 | "ng-table": "~0.3.3", 19 | "angular-spinner": "~0.5.1", 20 | "angular-ui-utils": "bower", 21 | "bootswatch-dist": "3.2.0-yeti", 22 | "d3": "~3.4.11", 23 | "cal-heatmap": "~3.5.2", 24 | "moment": "~2.9.0", 25 | "moment-timezone": "~0.3.0", 26 | "angular-loading-bar": "~0.7.0", 27 | "angular-motion": "~0.3.4", 28 | "angular-tablesort": "~1.0.5" 29 | }, 30 | "devDependencies": { 31 | "angular-mocks": "1.3.13", 32 | "angular-scenario": "1.3.13" 33 | }, 34 | "appPath": "app" 35 | } 36 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcmetrometrics", 3 | "version": "2.0.0", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "connect-modrewrite": "^0.7.9", 7 | "grunt": "^0.4.1", 8 | "grunt-autoprefixer": "^0.7.3", 9 | "grunt-concurrent": "^0.5.0", 10 | "grunt-contrib-clean": "^0.5.0", 11 | "grunt-contrib-concat": "^0.4.0", 12 | "grunt-contrib-connect": "^0.7.1", 13 | "grunt-contrib-copy": "^0.5.0", 14 | "grunt-contrib-cssmin": "^0.9.0", 15 | "grunt-contrib-htmlmin": "^0.3.0", 16 | "grunt-contrib-imagemin": "^0.7.0", 17 | "grunt-contrib-jshint": "^0.10.0", 18 | "grunt-contrib-uglify": "^0.4.0", 19 | "grunt-contrib-watch": "^0.6.1", 20 | "grunt-filerev": "^0.2.1", 21 | "grunt-google-cdn": "^0.4.0", 22 | "grunt-html-snapshot": "^0.6.1", 23 | "grunt-karma": "^0.8.3", 24 | "grunt-newer": "^0.7.0", 25 | "grunt-ngmin": "^0.0.3", 26 | "grunt-svgmin": "^0.4.0", 27 | "grunt-usemin": "^2.1.1", 28 | "grunt-wiredep": "^1.7.0", 29 | "jshint-stylish": "^0.2.0", 30 | "karma": "^0.12.19", 31 | "karma-jasmine": "^0.1.5", 32 | "karma-phantomjs-launcher": "^0.1.4", 33 | "load-grunt-tasks": "^0.4.0", 34 | "time-grunt": "^0.3.1" 35 | }, 36 | "engines": { 37 | "node": ">=0.10.0" 38 | }, 39 | "scripts": { 40 | "test": "grunt test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/peg/pegBuild.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys, os, subprocess, shutil 3 | 4 | this_dir = os.path.split(os.path.abspath(__file__))[0] 5 | target_dir = os.path.abspath(os.path.join(this_dir, "..", "app", "scripts")) 6 | 7 | input_file_name = "rankingsSearchString.pegjs" 8 | output_file_name = "rankingsSearchString.js" 9 | 10 | input_file = os.path.join(this_dir, input_file_name) 11 | output_file = os.path.join(this_dir, output_file_name) 12 | temp_file = os.path.join(this_dir, ".tmp.out") 13 | 14 | cmd = "pegjs -e searchStringParser %s %s"%(input_file, temp_file) 15 | ret = subprocess.call(cmd.split()) 16 | assert(ret == 0) 17 | 18 | script = open(temp_file).read().strip() 19 | 20 | # Rewrite the file to use strict. 21 | template = """"use strict"; 22 | var {script}""" 23 | 24 | # Write to output js file. 25 | output = template.format(script = script) 26 | with open(output_file, 'w') as fout: 27 | fout.write(output) 28 | print "Wrote to %s"%output_file 29 | 30 | # Copy the output file 31 | shutil.copy(output_file, target_dir) 32 | 33 | # Delete the temporary file 34 | os.remove(temp_file) 35 | 36 | print "Copied to %s"%target_dir 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /client/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-07-27 using 4 | // generator-karma 0.8.3 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | 'bower_components/angular/angular.js', 22 | 'bower_components/angular-mocks/angular-mocks.js', 23 | 'bower_components/angular-animate/angular-animate.js', 24 | 'bower_components/angular-cookies/angular-cookies.js', 25 | 'bower_components/angular-resource/angular-resource.js', 26 | 'bower_components/angular-route/angular-route.js', 27 | 'bower_components/angular-sanitize/angular-sanitize.js', 28 | 'bower_components/angular-touch/angular-touch.js', 29 | 'app/scripts/**/*.js', 30 | 'test/mock/**/*.js', 31 | 'test/spec/**/*.js' 32 | ], 33 | 34 | // list of files / patterns to exclude 35 | exclude: [], 36 | 37 | // web server port 38 | port: 8080, 39 | 40 | // Start these browsers, currently available: 41 | // - Chrome 42 | // - ChromeCanary 43 | // - Firefox 44 | // - Opera 45 | // - Safari (only Mac) 46 | // - PhantomJS 47 | // - IE (only Windows) 48 | browsers: [ 49 | 'PhantomJS' 50 | ], 51 | 52 | // Which plugins to enable 53 | plugins: [ 54 | 'karma-phantomjs-launcher', 55 | 'karma-jasmine' 56 | ], 57 | 58 | // Continuous Integration mode 59 | // if true, it capture browsers, run tests and exit 60 | singleRun: false, 61 | 62 | colors: true, 63 | 64 | // level of logging 65 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 66 | logLevel: config.LOG_INFO, 67 | 68 | // Uncomment the following lines if you are using grunt's server to run the tests 69 | // proxies: { 70 | // '/': 'http://localhost:9000/' 71 | // }, 72 | // URL root prevent conflicts with the site root 73 | // urlRoot: '_karma_' 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /client/test/spec/controllers/about.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: AboutCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('clientApp')); 7 | 8 | var AboutCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | AboutCtrl = $controller('AboutCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('clientApp')); 7 | 8 | var MainCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | MainCtrl = $controller('MainCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/test/spec/controllers/station.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: StationCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | var StationCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | StationCtrl = $controller('StationCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/test/spec/controllers/stationdirectory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: StationdirectoryCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | var StationdirectoryCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | StationdirectoryCtrl = $controller('StationdirectoryCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/test/spec/controllers/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: UnitCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | var UnitCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | UnitCtrl = $controller('UnitCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/test/spec/directives/linecolors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: lineColors', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element(''); 17 | element = $compile(element)(scope); 18 | expect(element.text()).toBe('this is the lineColors directive'); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /client/test/spec/filters/duration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Filter: duration', function () { 4 | 5 | // load the filter's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | // initialize a new instance of the filter before each test 9 | var duration; 10 | beforeEach(inject(function ($filter) { 11 | duration = $filter('duration'); 12 | })); 13 | 14 | it('should return the input prefixed with "duration filter:"', function () { 15 | var text = 'angularjs'; 16 | expect(duration(text)).toBe('duration filter: ' + text); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /client/test/spec/filters/unitidtohuman.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Filter: unitIdToHuman', function () { 4 | 5 | // load the filter's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | // initialize a new instance of the filter before each test 9 | var unitIdToHuman; 10 | beforeEach(inject(function ($filter) { 11 | unitIdToHuman = $filter('unitIdToHuman'); 12 | })); 13 | 14 | it('should return the input prefixed with "unitIdToHuman filter:"', function () { 15 | var text = 'angularjs'; 16 | expect(unitIdToHuman(text)).toBe('unitIdToHuman filter: ' + text); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /client/test/spec/services/directory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: directory', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | // instantiate service 9 | var directory; 10 | beforeEach(inject(function (_directory_) { 11 | directory = _directory_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!directory).toBe(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /client/test/spec/services/statustableutils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: statusTableUtils', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | // instantiate service 9 | var statusTableUtils; 10 | beforeEach(inject(function (_statusTableUtils_) { 11 | statusTableUtils = _statusTableUtils_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!statusTableUtils).toBe(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /client/test/spec/services/unitservice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: unitService', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('dcmetrometricsApp')); 7 | 8 | // instantiate service 9 | var unitService; 10 | beforeEach(inject(function (_unitService_) { 11 | unitService = _unitService_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!unitService).toBe(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /data/codes.tsv: -------------------------------------------------------------------------------- 1 | 3294 TURNED OFF/WALKER 2 | 3430 CALLBACK/REPAIR 3 | 2734 PREV. MAINT. INSPECTION 4 | 3366 WEATHER RELATED 5 | 3434 REHAB/MODERNIZATION 6 | 2096 INCIDENT/ACCIDENT 7 | 2720 FIRE ALARM/DELUGE SYSTEMS 8 | 2914 SCHEDULED SUPPORT 9 | 2723 POWER SURGE/OUTAGE 10 | 4300 SAFETY WORK ORDER 11 | 0003 MINOR REPAIR 12 | 3359 WATER LEAK/INTRUSION 13 | 2907 SAFETY INSPECTION 14 | 5050 HANDRAIL 15 | 0005 MAJOR REPAIR 16 | 2733 PREV. MAINT. REPAIRS 17 | 3433 PREV. MAINT. COMPLIANCE INSPECTION 18 | -------------------------------------------------------------------------------- /data/test_data/data.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/data/test_data/data.ods -------------------------------------------------------------------------------- /data/test_data/data.tsv: -------------------------------------------------------------------------------- 1 | UnitName UnitType StationCode StationName LocationDescription SymptomCode SymptomDescription TickOffset 2 | A03N01 ESCALATOR A03 Dupont Circle, North Escalator to street 2 Lazy 10 3 | A03N02 ESCALATOR A03 Dupont Circle, North Escalator to street 3 Slippery 10 4 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 4 Power 20 5 | A03N02 ESCALATOR A03 Dupont Circle, North Escalator to street 3 Slippery 20 6 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 4 Power 30 7 | A03N02 ESCALATOR A03 Dupont Circle, North Escalator to street 3 Slippery 30 8 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 4 Power 40 9 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 5 PowerStillOff 50 10 | A03N01 ESCALATOR A03 Dupont Circle, North Escalator to street 2 Lazy 60 11 | A03N02 ESCALATOR A03 Dupont Circle, North Escalator to street 3 Slippery 60 12 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 4 Power 70 13 | A03N02 ESCALATOR A03 Dupont Circle, North Escalator to street 3 Slippery 70 14 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 4 Power 80 15 | A03N02 ESCALATOR A03 Dupont Circle, North Escalator to street 3 Slippery 80 16 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 4 Power 90 17 | A02S01 ESCALATOR A02 Farragut North, 17th Street Escalator to street 5 PowerStillOff 100 18 | -------------------------------------------------------------------------------- /data/test_data/data.txt: -------------------------------------------------------------------------------- 1 | UnitName UnitType StationCode StationName LocationDescription SymptomCode SymptomDescription Group A03N01ESCALATOR ESCALATOR A03 "Dupont Circle, North" Escalator to street 2 Lazy 1 A03N02ESCALATOR ESCALATOR A03 "Dupont Circle, North" Escalator to street 3 Slippery 1 A02X012ESCALATOR ESCALATOR A02 "Farragut North, 17th Street" Escalator to street 4 Power 2 A03N02ESCALATOR ESCALATOR A03 "Dupont Circle, North" Escalator to street 3 Slippery 2 A02X012ESCALATOR ESCALATOR A02 "Farragut North, 17th Street" Escalator to street 4 Power 3 A03N02ESCALATOR ESCALATOR A03 "Dupont Circle, North" Escalator to street 3 Slippery 3 A02X012ESCALATOR ESCALATOR A02 "Farragut North, 17th Street" Escalator to street 4 Power 4 -------------------------------------------------------------------------------- /data/test_data/data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/data/test_data/data.xlsx -------------------------------------------------------------------------------- /dcmetrometrics/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: dcmetrometrics 3 | 4 | This python package defines classes/functions: 5 | - to pull WMATA data from the WMATA API and Twitter 6 | - to process and store this data in a MongoDB database 7 | - to generate the DC Metro Metrics website 8 | - to run the automated Twitter accounts @MetroHotCars, @MetroEscalators, @MetroElevators. 9 | 10 | The package is organized as follows: 11 | - eles: package for escalator/elevator data 12 | - common: package for common utility classes/functions used throughout 13 | the application. 14 | - hotcars: package for #wmata #hotcar data 15 | - web: package for generating the DC Metro Metrics website 16 | - third_party: third party packages used by this application. 17 | """ 18 | 19 | from .common import * 20 | -------------------------------------------------------------------------------- /dcmetrometrics/common/DataWriteable.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | 4 | def convert(k, v): 5 | """Convert a k,v pair into a dictionary. 6 | If v is a dictionary, it will be flattened 7 | """ 8 | if isinstance(v, (int, float, str, unicode, list)) or v is None: 9 | return {k: v} 10 | elif isinstance(v, long): 11 | return {k: unicode(v)} # Return as unicode to prevent loss of precision 12 | elif isinstance(v, (date, datetime)): 13 | return {k: v.isoformat()} 14 | elif isinstance(v, dict): 15 | ret = {} 16 | for k2, v2 in v.iteritems(): 17 | k3 = '%s_%s'%(k, k2) # flatten the keys 18 | ret.update(convert(k3, v2)) 19 | return ret 20 | elif isinstance(v, DataWriteable): 21 | ret = {} 22 | dr = v.to_data_record() 23 | for k2, v2 in dr.iteritems(): 24 | k3 = '%s_%s'%(k, k2) # flatten the keys 25 | ret.update(convert(k3, v2)) 26 | return ret 27 | 28 | raise TypeError("Cannot convert value of %s"%type(v)) 29 | 30 | class DataWriteable(object): 31 | """This will flatten objects into a single dictionary 32 | """ 33 | 34 | def to_data_record(self, keep_na = True): 35 | 36 | fields = getattr(self, 'data_fields', []) 37 | ret = {} 38 | 39 | for k in fields: 40 | 41 | v = getattr(self, k, None) 42 | 43 | if keep_na: 44 | ret.update(convert(k, v)) 45 | elif v is not None: 46 | ret.update(convert(k, v)) 47 | 48 | return ret -------------------------------------------------------------------------------- /dcmetrometrics/common/WebJSONMixin.py: -------------------------------------------------------------------------------- 1 | class WebJSONMixin(object): 2 | 3 | def to_web_json(self): 4 | 5 | fields = getattr(self, 'web_json_fields', []) 6 | 7 | return dict((k, getattr(self, k, None)) for k in fields) -------------------------------------------------------------------------------- /dcmetrometrics/common/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides defines classes and functions that 3 | are used throughout the application. 4 | """ 5 | -------------------------------------------------------------------------------- /dcmetrometrics/common/descriptors.py: -------------------------------------------------------------------------------- 1 | # descriptors.py 2 | # Defines class descriptors 3 | # 4 | # setOnce: descriptor for a class attribute that can be set 5 | # once, and then is read-only thereafter. 6 | # 7 | # computeOnce: descriptor for a class attribute that should be 8 | # computed once on the first access. All future accesses 9 | # returned the computed value. This is similar to using 10 | # the @property decorator. 11 | 12 | 13 | ###################################################### 14 | # Descriptor to define a read only attribute 15 | # which can only be set once. 16 | class setOnce(object): 17 | def __init__(self, name, default = None): 18 | self.name = "_" + name # Need leading underscore 19 | self.default = default 20 | 21 | def __get__(self, instance, cls): 22 | if not instance: 23 | raise AttributeError('Can only access property through instance') 24 | return getattr(instance, self.name, self.default) 25 | 26 | # Only allow the attribute to be set once 27 | def __set__(self, instance, value): 28 | if not getattr(instance, self.name, None): 29 | setattr(instance, self.name, value) 30 | return 31 | raise AttributeError('Attribute is read-only.') 32 | 33 | def __delete__(self, instance): 34 | raise AttributeError('Attribute is read-only') 35 | 36 | ###################################################### 37 | # Descriptor to define a read only attribute 38 | # which should only be computed once on the first access. 39 | # 40 | # The advantage of using this descriptor is that if the 41 | # class attribute is never used, it is never computed, and 42 | # if it is used, it is only computed once. 43 | class computeOnce(object): 44 | count = 0 # used to assign a unique variable name to store 45 | # value within an instance 46 | 47 | def __init__(self, fget): 48 | self.name = "_" + 'computeOnce%i'%computeOnce.count 49 | computeOnce.count = computeOnce.count + 1 50 | self.fget = fget 51 | 52 | def __get__(self, instance, cls): 53 | if not instance: 54 | raise AttributeError('Can only access property through instance') 55 | 56 | retVal = getattr(instance, self.name, None) 57 | if retVal is None: 58 | retVal = self.fget(instance) 59 | setattr(instance, self.name, retVal) 60 | return retVal 61 | 62 | def __set__(self, instance, value): 63 | raise AttributeError('Attribute is read-only.') 64 | 65 | def __delete__(self, instance): 66 | raise AttributeError('Attribute is read-only') 67 | -------------------------------------------------------------------------------- /dcmetrometrics/common/globals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Define global variables. 3 | """ 4 | 5 | import os 6 | 7 | PY_DIR = os.environ['PYTHON_DIR'] 8 | REPO_DIR = os.environ['REPO_DIR'] 9 | SCRIPT_DIR = os.path.join(REPO_DIR, 'scripts') 10 | DATA_DIR = os.environ['DATA_DIR'] 11 | WWW_DIR = os.environ['WWW_DIR'] 12 | 13 | MONGODB_HOST = os.environ["MONGODB_HOST"] 14 | MONGODB_PORT = int(os.environ["MONGODB_PORT"]) 15 | MONGODB_USERNAME = os.environ.get("MONGODB_USERNAME", None) 16 | MONGODB_PASSWORD = os.environ.get("MONGODB_PASSWORD", None) 17 | 18 | INTERNAL_SERVE_IP = os.environ["INTERNAL_SERVE_IP"] # Internal IP Address to serve app through. 19 | INTERNAL_SERVE_PORT = os.environ["INTERNAL_SERVE_PORT"] # Internal Port to serve app through. -------------------------------------------------------------------------------- /dcmetrometrics/common/logging_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for creating logger objects. 3 | """ 4 | import logging, sys 5 | 6 | 7 | def create_logger(name): 8 | 9 | # Get the logger 10 | logger = logging.getLogger(name) 11 | 12 | # Reset handlers on the logger. 13 | logger.handlers = [] 14 | 15 | # Create a stream handler 16 | sh = logging.StreamHandler(stream=sys.stderr) 17 | 18 | # create formatter 19 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 20 | 21 | # add formatter to stream handler 22 | sh.setFormatter(formatter) 23 | 24 | # add stream handler to logger 25 | logger.addHandler(sh) 26 | 27 | # set logging level 28 | logger.setLevel(logging.DEBUG) 29 | 30 | return logger -------------------------------------------------------------------------------- /dcmetrometrics/common/restartingGreenlet.py: -------------------------------------------------------------------------------- 1 | """ 2 | common.restartingGreenlet 3 | 4 | Extend the gevent Greenlet class so that the Greenlet restarts whenever it finishes. 5 | """ 6 | 7 | import sys 8 | from gevent import Greenlet, sleep 9 | from datetime import datetime 10 | 11 | def makeNewGreenlet(g): 12 | args = g.rsargs 13 | kwargs = g.rskwargs 14 | newg = g.__class__(*args, **kwargs) 15 | t = type(newg) 16 | gid = str(newg) 17 | dateStr = str(datetime.now()) 18 | sys.stderr.write('%s: Making new greenlet %s of type %s\n'%(dateStr, gid, str(t))) 19 | 20 | # Impose a short sleep. This is to prevent a buggy RestartingGreenlet 21 | # from perpetually throwing an exception and continually restarting 22 | # TO DO: Consider a max restarts parameter 23 | sleep(1) 24 | return newg 25 | 26 | class RestartingGreenlet(Greenlet): 27 | 28 | def __init__(self, *args, **kwargs): 29 | Greenlet.__init__(self) 30 | 31 | # Save the constructer arguments 32 | # so we can recreate the Greenlet 33 | self.rsargs = args 34 | self.rskwargs = kwargs 35 | 36 | # Set up this Greenlet to use the restarter 37 | self.link(self.restart) 38 | 39 | @staticmethod 40 | def restart(g): 41 | newg = makeNewGreenlet(g) 42 | newg.start() 43 | -------------------------------------------------------------------------------- /dcmetrometrics/common/twitterUtils.py: -------------------------------------------------------------------------------- 1 | """ 2 | common.twitterUtils 3 | 4 | """ 5 | import twitter 6 | from twitter import TwitterError 7 | 8 | TWITTER_TIMEOUT = 10 9 | 10 | def getApi(keys): 11 | """ 12 | Construct a Twitter API instance. 13 | """ 14 | api = twitter.Api(consumer_key = keys.consumer_key, 15 | consumer_secret = keys.consumer_secret, 16 | access_token_key = keys.access_token, 17 | access_token_secret = keys.access_token_secret, 18 | cache = None, 19 | requests_timeout = TWITTER_TIMEOUT) 20 | return api 21 | -------------------------------------------------------------------------------- /dcmetrometrics/eles/WMATA_API.py: -------------------------------------------------------------------------------- 1 | """ 2 | eles.wmataAPI 3 | 4 | Request data from the WMATA API 5 | """ 6 | 7 | import requests 8 | 9 | class WMATA_API_ERROR(Exception): 10 | def __init__(self, requestObj): 11 | self.requestObj = requestObj 12 | def __str__(self): 13 | return 'Url=%s, status_code=%i'%(self.requestObj.url, self.requestObj.status_code) 14 | 15 | class WMATA_API(object): 16 | """ 17 | (partial) implementation of the WMATA API. 18 | """ 19 | 20 | def __init__(self, key): 21 | if not isinstance(key, str) or not key: 22 | raise TypeError('WMATA_API key should be str') 23 | 24 | self.API_KEY = key 25 | self.URL_BASE = 'http://api.wmata.com' 26 | self.TIMEOUT = 10 # timeout requests after 10 seconds. 27 | 28 | # Check if a request is okay. If it isn't raise WMATA_API_ERROR 29 | def checkRequest(self, req): 30 | if req.status_code != requests.codes.ok: 31 | raise WMATA_API_ERROR(req) 32 | 33 | def request(self, url, params): 34 | payload = { 'api_key' : self.API_KEY } 35 | headers = { 'api_key' : self.API_KEY } 36 | if params is not None: 37 | payload.update(params) 38 | r = requests.get(url, params=payload, timeout = self.TIMEOUT, headers = headers) 39 | self.checkRequest(r) 40 | return r 41 | 42 | # Request the static webpage with elevator/escalator status 43 | def getEscalatorWebpageStatus(self): 44 | url = 'http://www.wmata.com/rider_tools/metro_service_status/elevator_escalator.cfm' 45 | r = requests.get(url, timeout = self.TIMEOUT) 46 | return r 47 | 48 | def getStations(self, params = None): 49 | base = '%s/Rail.svc/json'%self.URL_BASE 50 | url = '{base}/JStations'.format(base=base) 51 | return self.request(url, params) 52 | 53 | def getLines(self, params = None): 54 | base = '%s/Rail.svc/json'%self.URL_BASE 55 | url = '{base}/JLines'.format(base=base) 56 | return self.request(url, params) 57 | 58 | def getStationInfo(self, params = None): 59 | base = '%s/Rail.svc/json'%self.URL_BASE 60 | url = '{base}/JStationInfo'.format(base=base) 61 | return self.request(url, params) 62 | 63 | def getIncidents(self, params = None): 64 | base = '%s/Incidents.svc/json'%(self.URL_BASE) 65 | url = '{base}/Incidents'.format(url) 66 | return self.request(url, params) 67 | 68 | def getEscalator(self, params = None): 69 | base = '%s/Incidents.svc/json'%(self.URL_BASE) 70 | url = '{base}/ElevatorIncidents'.format(base=base) 71 | return self.request(url, params) 72 | -------------------------------------------------------------------------------- /dcmetrometrics/eles/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modules for working with WMATA escalator & elevator data. 3 | """ 4 | -------------------------------------------------------------------------------- /dcmetrometrics/eles/defs.py: -------------------------------------------------------------------------------- 1 | """ 2 | eles.defs 3 | 4 | Define constants for escalator/elevator status codes 5 | """ 6 | 7 | from collections import defaultdict 8 | 9 | OPERATIONAL_CODE = -1 10 | NUM_ESCALATORS = 588 11 | NUM_ELEVATORS = 238 12 | 13 | ################################################### 14 | # Define symptomToCategory. Consider adding these 15 | # to the symptom_codes database 16 | symptomToCategory = defaultdict(lambda: 'BROKEN') 17 | 18 | offStatuses = ['SAFETY WORK ORDER', 19 | 'TURNED OFF/WALKER', 20 | 'SCHEDULED SUPPORT', 21 | 'PREV. MAINT. REPAIRS', 22 | 'Inspection Repair', 23 | 'Walker', 24 | 'Scheduled Support', 25 | 'Preventive Maintenance Repairs'] 26 | 27 | inspectStatuses = ['SAFETY INSPECTION', 28 | 'PREV. MAINT. INSPECTION', 29 | 'PREV. MAINT. COMPLIANCE INSPECTION', 30 | 'Preventive Maintenance Inspection', 31 | 'Safety Inspection'] 32 | 33 | rehabStatuses = ['REHAB/MODERNIZATION', 'Modernization'] 34 | 35 | 36 | symptomToCategory['OPERATIONAL'] = 'ON' 37 | symptomToCategory.update((status, 'OFF') for status in offStatuses) 38 | symptomToCategory.update((status, 'INSPECTION') for status in inspectStatuses) 39 | symptomToCategory.update((status, 'REHAB') for status in rehabStatuses) 40 | 41 | SYMPTOM_CHOICES = ('ON', 'OFF', 'INSPECTION', 'REHAB', 'BROKEN') 42 | 43 | -------------------------------------------------------------------------------- /dcmetrometrics/eles/escalatorRequest.py: -------------------------------------------------------------------------------- 1 | """ 2 | eles.esclatorRequest 3 | 4 | Get escalator incidents through the wmata API 5 | """ 6 | 7 | # Python modules 8 | import time 9 | import os 10 | import sys 11 | import cPickle 12 | from collections import defaultdict, Counter 13 | from datetime import datetime 14 | 15 | # Custom modules 16 | from ..common import stations 17 | from ..keys import WMATA_API_KEY 18 | from .WMATA_API import WMATA_API, WMATA_API_ERROR 19 | from .Incident import Incident 20 | 21 | api = None 22 | def getAPI(): 23 | global api 24 | if WMATA_API_KEY is None: 25 | return None 26 | if api is not None: 27 | return api 28 | api = WMATA_API(key=WMATA_API_KEY) 29 | return api 30 | 31 | # Summarize results to standard output 32 | def summarize(result): 33 | """ 34 | Summarize the escalator/elevator statuses to standard output. 35 | """ 36 | incidents = result['incidents'] 37 | numIncidents = len(incidents) 38 | timeStr = time.strftime('%d_%b_%Y-%H_%M_%S', result['requestTime']) 39 | sys.stdout.write('Time: %s\n'%timeStr) 40 | 41 | sys.stdout.write('\n\n') 42 | sys.stdout.write('Num Incidents: %i\n'%numIncidents) 43 | 44 | symptomCounts = Counter(i['SymptomDescription'] for i in incidents) 45 | maxWidth = max(len(s) for s in symptomCounts.keys()) 46 | formatStr = '{symptom:%is} {count:d}\n'%maxWidth 47 | for symptom, count in symptomCounts.most_common(): 48 | outS = formatStr.format(symptom=symptom, count=count) 49 | sys.stdout.write(outS) 50 | sys.stdout.write('\n\n') 51 | 52 | # Gather incidents by station Code 53 | stationToIncident = defaultdict(list) 54 | for d in incidents: 55 | stationToIncident[d['StationCode']].append(d) 56 | for code in sorted(stations.allCodes): 57 | stationIncidents = stationToIncident.get(code, []) 58 | sys.stdout.write('%s (%s): %i\n'%(stations.codeToName[code], code, len(stationIncidents))) 59 | 60 | def run(): 61 | """ 62 | Dump all incidents to a pickle file. 63 | """ 64 | requestTime = time.localtime() 65 | timeStr = time.strftime('%d_%b_%Y-%H_%M_%S', requestTime) 66 | api = getAPI() 67 | res = api.getEscalator() 68 | incidents = res.json()['ElevatorIncidents'] 69 | 70 | websiteTxt = Req.getEscalatorWebpageStatus().text 71 | 72 | result = { 'incidents' : incidents, 73 | 'requestTime' : time.localtime(), 74 | 'webpage' : websiteTxt } 75 | 76 | summarize(result) 77 | 78 | fname = '%s.pickle'%(timeStr) 79 | fout = open(fname, 'w') 80 | cPickle.dump(result, fout) 81 | fout.close() 82 | 83 | # Make a request for the twitter app 84 | 85 | if __name__ == '__main__': 86 | run() 87 | -------------------------------------------------------------------------------- /dcmetrometrics/eles/misc_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miscellaneous utility functions 3 | """ 4 | 5 | import itertools 6 | from operator import itemgetter, attrgetter 7 | 8 | from ..common.metroTimes import isNaive 9 | 10 | def get_one(cursor): 11 | """ 12 | Get one item from the iterable. 13 | Return None if there is None. 14 | """ 15 | try: 16 | return next(cursor) 17 | except StopIteration: 18 | return None 19 | 20 | def get_some(cursor, N): 21 | """ 22 | Get at most N items from the cursor, and return as a list 23 | """ 24 | return [i for i in itertools.slice(cursor, N)] 25 | 26 | def get_first_status_since(statusList, time): 27 | """ 28 | Get the first status in the status list that starts after time. 29 | """ 30 | statusList = sorted(statusList, key = attrgetter('time')) # in time ascending 31 | myRecs = (rec for rec in statusList if rec.time > time) 32 | return get_one(myRecs) 33 | 34 | ############################################################# 35 | def checkAllTimesNotNaive(statusList): 36 | for s in statusList: 37 | if isNaive(s.time): 38 | raise RuntimeError('Times cannot be naive') 39 | end_time = getattr(s, 'end_time', None) 40 | if end_time and isNaive(end_time): 41 | raise RuntimeError('Times cannot be naive') 42 | 43 | ############################################################# 44 | # Check that each status has a 'time' and 'end_time' defined 45 | # and that the list is sorted 46 | def checkStatusListSane(statusList): 47 | lastTime = None 48 | for s in statusList: 49 | if not getattr(s, 'end_time', None): 50 | raise RuntimeError('Status missing end_time') 51 | if not getattr(s, 'time', None): 52 | raise RuntimeError('Status missing time') 53 | if s.time > s.end_time: 54 | raise RuntimeError('Status has bad starting/ending time') 55 | if lastTime and s.time < lastTime: 56 | raise RuntimeError('Status not sorted properly') 57 | lastTime = s.time 58 | 59 | def yieldNothing(): 60 | return 61 | yield -------------------------------------------------------------------------------- /dcmetrometrics/hotcars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/dcmetrometrics/hotcars/__init__.py -------------------------------------------------------------------------------- /dcmetrometrics/hotcars/twitter_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | getTwitterAPI: Return the python_twitter 3 | """ 4 | from ..common import twitterUtils 5 | from twitter import TwitterError 6 | 7 | T = None 8 | def getTwitterAPI(): 9 | global T 10 | 11 | if T is None: 12 | from ..keys import HotCarKeys, MissingKeyError 13 | 14 | # Check that the HotCar Twitter API Keys have been set. 15 | if not HotCarKeys.isSet(): 16 | msg = \ 17 | """ 18 | 19 | The HotCar App requires that the HotCarKeys be set. 20 | Check your keys.py. 21 | 22 | For more information, see: 23 | https://github.com/LeeMendelowitz/DCMetroMetrics/wiki/API-Keys 24 | 25 | """ 26 | raise MissingKeyError(msg) 27 | 28 | T = twitterUtils.getApi(HotCarKeys) 29 | return T -------------------------------------------------------------------------------- /dcmetrometrics/keys/__init__.py: -------------------------------------------------------------------------------- 1 | from .key_utils import keyModuleError, MissingKeyError 2 | 3 | try: 4 | from .keys import MetroEscalatorKeys,\ 5 | MetroElevatorKeys,\ 6 | HotCarKeys,\ 7 | WUNDERGROUND_API_KEY,\ 8 | WMATA_API_KEY,\ 9 | MissingKeyError,\ 10 | TwitterKeyError 11 | 12 | except ImportError as e: 13 | 14 | # The key module is not properly defined. 15 | keyModuleError() 16 | 17 | -------------------------------------------------------------------------------- /dcmetrometrics/keys/key_utils.py: -------------------------------------------------------------------------------- 1 | class MissingKeyError(Exception): 2 | pass 3 | 4 | class TwitterKeyError(MissingKeyError): 5 | pass 6 | 7 | class TwitterKeys(object): 8 | @classmethod 9 | def checkKeys(cls): 10 | """ 11 | Check that the twitter keys have been properly set. 12 | """ 13 | req = ['consumer_key', 'access_token', 14 | 'consumer_secret', 'access_token_secret'] 15 | for k in req: 16 | val = getattr(cls, k, None) 17 | if (not val) or (not isinstance(val, str)): 18 | msg = 'Twitter Keys {name} attribute {attr} is not properly set. Check your keys.py' 19 | msg = msg.format(name = cls.__name__, attr=k) 20 | raise TwitterKeyError(msg) 21 | @classmethod 22 | def isSet(cls): 23 | req = ['consumer_key', 'access_token', 24 | 'consumer_secret', 'access_token_secret'] 25 | keysAreSet = True 26 | for k in req: 27 | val = getattr(cls, k, None) 28 | if (not val) or (not isinstance(val, str)): 29 | keysAreSet = False 30 | return keysAreSet 31 | 32 | def keyModuleError(): 33 | """ 34 | Raise a RuntimeError due to a missing keys module. 35 | """ 36 | 37 | msg = \ 38 | """ 39 | 40 | Could not import dcmetrometrics.keys module because the file is missing 41 | or is malformed. 42 | 43 | You must create the keys module using the provided template: 44 | 45 | cp dcmetrometrics/keys/keys_default.py dcmetrometrics/keys/keys.py 46 | 47 | For more information, see: 48 | https://github.com/LeeMendelowitz/DCMetroMetrics/wiki/API-Keys 49 | 50 | """ 51 | raise RuntimeError(msg) 52 | -------------------------------------------------------------------------------- /dcmetrometrics/keys/keys_default.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is a template for storing the API Keys used 3 | by this application. 4 | 5 | Copy this file into keys.py, and replace the None values with 6 | str values of your own keys. 7 | 8 | Required Keys 9 | ============= 10 | - The EscalatorApp and the ElevatorApp require the WMATA_API_KEY to run. 11 | - The HotCarsApp requires the HotCarKeys to seach for hot car tweets and to live tweet. 12 | 13 | Optional keys 14 | ============= 15 | - The EscalatorApp needs the MetroEscalatorKeys to live tweet, otherwise 16 | tweet messages will only be written to a log file. 17 | - The ElevatorApp needs MetroElevatorKeys to live tweet, otherwise 18 | tweet message will only be written to a log file. 19 | - The WebPageGeneratorApp needs the WundergroundAPI to include DC temperature 20 | history on the hotcars webpage, otherwise default temperature values are used. 21 | """ 22 | 23 | from .key_utils import * 24 | 25 | class MetroEscalatorKeys(TwitterKeys): 26 | """ 27 | Key for @MetroEscalators Twitter acccount 28 | """ 29 | consumer_key = None 30 | access_token = None 31 | consumer_secret = None 32 | access_token_secret = None 33 | 34 | class MetroElevatorKeys(TwitterKeys): 35 | """ 36 | Key for @MetroElevators Twitter acccount 37 | """ 38 | consumer_key = None 39 | access_token = None 40 | consumer_secret = None 41 | access_token_secret = None 42 | 43 | class HotCarKeys(TwitterKeys): 44 | """ 45 | Key for @MetroHotCars Twitter account 46 | """ 47 | consumer_key = None 48 | consumer_secret = None 49 | access_token = None 50 | access_token_secret = None 51 | 52 | WUNDERGROUND_API_KEY = None 53 | 54 | WMATA_API_KEY = None 55 | -------------------------------------------------------------------------------- /dcmetrometrics/third_party/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Third party modules. 3 | """ 4 | 5 | from . import twitter, gviz_api 6 | -------------------------------------------------------------------------------- /elevatorApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | apps.runElevatorApp 4 | 5 | # This runs the elevatorApp instance. 6 | # This app downloads elevator data from the WMATA API, 7 | # stores elevator statuses in the database, and 8 | # generates tweets for @MetroElevators. 9 | 10 | # This module can be executed directly for local testing 11 | """ 12 | 13 | if __name__ == "__main__": 14 | # Local Testing 15 | import test.setup 16 | 17 | # python imports 18 | import os 19 | import sys 20 | import subprocess 21 | from datetime import datetime 22 | import gevent 23 | from gevent import Greenlet 24 | 25 | # custom imports 26 | from dcmetrometrics.common.globals import DATA_DIR, REPO_DIR, DATA_DIR 27 | from dcmetrometrics.common.restartingGreenlet import RestartingGreenlet 28 | from dcmetrometrics.eles.ElevatorApp import ElevatorApp as App 29 | 30 | OUTPUT_DIR = DATA_DIR 31 | if OUTPUT_DIR is None: 32 | OUTPUT_DIR = os.getcwd() 33 | 34 | if REPO_DIR is None: 35 | SCRIPT_DIR = os.getcwd() 36 | else: 37 | SCRIPT_DIR = os.path.join(REPO_DIR, 'scripts') 38 | 39 | SLEEP = 30 40 | 41 | ########################################## 42 | # Run the Twitter App as a Greenlet. 43 | class ElevatorApp(RestartingGreenlet): 44 | 45 | def __init__(self, SLEEP=SLEEP, LIVE=False): 46 | RestartingGreenlet.__init__(self, SLEEP=SLEEP, LIVE=LIVE) 47 | self.LIVE = LIVE # Tweet only if Live 48 | self.SLEEP = SLEEP # Sleep time after each tick 49 | self.logFileName = os.path.join(DATA_DIR, 'runElevatorApp.log') 50 | 51 | def _run(self): 52 | while True: 53 | try: 54 | self.tick() 55 | except Exception as e: 56 | import traceback 57 | logFile = open(self.logFileName, 'a') 58 | logFile.write('ElevatorApp caught Exception: %s\n'%(str(e))) 59 | tb = traceback.format_exc() 60 | logFile.write('Traceback:\n%s\n\n'%tb) 61 | logFile.close() 62 | gevent.sleep(self.SLEEP) 63 | 64 | def tick(self): 65 | 66 | # Run MetroElevators twitter App 67 | with open(self.logFileName, 'a') as logFile: 68 | 69 | n = datetime.now() 70 | timeStr = n.strftime('%d-%B-%Y %H:%M:%S') 71 | 72 | msg = '*'*50 + '\n' 73 | msg += '%s Elevator App Tick\n'%timeStr 74 | msg += 'App Mode: %s\n'%('LIVE' if self.LIVE else 'NOT LIVE') 75 | 76 | logFile.write(msg) 77 | logFile.flush() 78 | 79 | app = App(logFile, LIVE=self.LIVE) 80 | app.tick() 81 | 82 | if __name__ == "__main__": 83 | print 'Running the elevator app locally....' 84 | escApp = ElevatorApp() 85 | escApp.start() 86 | escApp.join() 87 | -------------------------------------------------------------------------------- /escalatorApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | apps.runEscalatorApp 4 | 5 | # This runs the escalatorApp instance. 6 | # This app downloads escalator data from the WMATA API, 7 | # stores escalator statuses in the database, and 8 | # generates tweets for @MetroEscalators. 9 | 10 | # This module can be executed directly for local testing 11 | """ 12 | 13 | if __name__ == "__main__": 14 | # Local Testing 15 | import test.setup 16 | 17 | import os 18 | import sys 19 | import subprocess 20 | from datetime import datetime 21 | import gevent 22 | from gevent import Greenlet 23 | 24 | from dcmetrometrics.common.restartingGreenlet import RestartingGreenlet 25 | from dcmetrometrics.eles.EscalatorApp import EscalatorApp as App 26 | from dcmetrometrics.common.globals import DATA_DIR, REPO_DIR 27 | 28 | OUTPUT_DIR = DATA_DIR 29 | if OUTPUT_DIR is None: 30 | OUTPUT_DIR = os.getcwd() 31 | 32 | if REPO_DIR is None: 33 | SCRIPT_DIR = os.getcwd() 34 | else: 35 | SCRIPT_DIR = os.path.join(REPO_DIR, 'scripts') 36 | 37 | SLEEP = 30 38 | 39 | ########################################## 40 | # Run the Twitter App as a Greenlet. 41 | class EscalatorApp(RestartingGreenlet): 42 | 43 | def __init__(self, SLEEP=SLEEP, LIVE=False): 44 | RestartingGreenlet.__init__(self, SLEEP=SLEEP, LIVE=LIVE) 45 | self.LIVE = LIVE # Tweet only if Live 46 | self.SLEEP = SLEEP # Sleep time after each tick 47 | self.logFileName = os.path.join(DATA_DIR, 'runEscalatorApp.log') 48 | 49 | def _run(self): 50 | while True: 51 | try: 52 | self.tick() 53 | except Exception as e: 54 | import traceback 55 | logFile = open(self.logFileName, 'a') 56 | logFile.write('EscalatorApp caught Exception: %s\n'%(str(e))) 57 | tb = traceback.format_exc() 58 | logFile.write('Traceback:\n%s\n\n'%tb) 59 | logFile.close() 60 | gevent.sleep(self.SLEEP) 61 | 62 | def tick(self): 63 | 64 | # Run MetroEsclaators twitter App 65 | with open(self.logFileName, 'a') as logFile: 66 | 67 | n = datetime.now() 68 | timeStr = n.strftime('%d-%B-%Y %H:%M:%S') 69 | 70 | msg = '*'*50 + '\n' 71 | msg += '%s Escalator App Tick\n'%timeStr 72 | msg += 'App Mode: %s\n'%('LIVE' if self.LIVE else 'NOT LIVE') 73 | 74 | logFile.write(msg) 75 | logFile.flush() 76 | 77 | app = App(logFile, LIVE=self.LIVE) 78 | app.tick() 79 | 80 | if __name__ == "__main__": 81 | print 'Running the escalator app locally....' 82 | escApp = EscalatorApp() 83 | escApp.start() 84 | escApp.join() 85 | -------------------------------------------------------------------------------- /hotCarApp.py: -------------------------------------------------------------------------------- 1 | """ 2 | apps.hotCarApp 3 | 4 | Define the HotCarApp as a restartingGreenlet. 5 | 6 | This app will use the Twitter API to search for tweets about #wmata 7 | #hotcar's. These tweets and the hotcar data are stored in a database. 8 | Tweet acknowledgements are posted to the @MetroHotCars twitter account. 9 | """ 10 | 11 | # TEST CODE 12 | if __name__ == "__main__": 13 | import test.setup 14 | 15 | import gevent 16 | import os 17 | import sys 18 | from datetime import datetime 19 | 20 | from dcmetrometrics.common.restartingGreenlet import RestartingGreenlet 21 | from dcmetrometrics.common import dbGlobals 22 | from dcmetrometrics.hotcars import hotCars 23 | from dcmetrometrics.common.globals import DATA_DIR, REPO_DIR, DATA_DIR 24 | 25 | OUTPUT_DIR = DATA_DIR 26 | 27 | ############################################################### 28 | # Log the HotCarApp App to a file. 29 | import logging 30 | 31 | LOG_FILE_NAME = os.path.join(DATA_DIR, 'HotCarApp.log') 32 | fh = logging.FileHandler(LOG_FILE_NAME) 33 | sh = logging.StreamHandler(sys.stderr) 34 | 35 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 36 | fh.setFormatter(formatter) 37 | sh.setFormatter(formatter) 38 | 39 | logger = logging.getLogger('HotCarApp') 40 | logger.addHandler(fh) 41 | logger.addHandler(sh) 42 | ################################################################# 43 | 44 | class HotCarApp(RestartingGreenlet): 45 | 46 | def __init__(self, LIVE=False): 47 | 48 | dbGlobals.connect() 49 | 50 | RestartingGreenlet.__init__(self, LIVE=LIVE) 51 | self.SLEEP = 40 # Run every 10 seconds 52 | self.LIVE = LIVE 53 | 54 | # Run forever 55 | def _run(self): 56 | 57 | while True: 58 | 59 | try: 60 | 61 | hotCars.tick(tweetLive = self.LIVE) 62 | 63 | except Exception as e: 64 | 65 | import traceback 66 | tb = traceback.format_exc() 67 | logger.error("HotCarApp Caught Error! %s\nTraceback:\n%s"%(str(e), tb)) 68 | 69 | gevent.sleep(self.SLEEP) 70 | 71 | 72 | 73 | if __name__ == '__main__': 74 | from time import sleep 75 | app = HotCarApp(LIVE = False) 76 | app.start() 77 | app.join() 78 | 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bottle==0.12.5 2 | gevent==1.0.1 3 | gnureadline==6.3.3 4 | greenlet==0.4.5 5 | gunicorn==18.0 6 | httplib2==0.8 7 | ipython==2.3.1 8 | mongoengine==0.8.7 9 | numpy==1.9.1 10 | oauth2==1.5.211 11 | oauthlib==0.7.2 12 | pandas==0.15.2 13 | pymongo==2.6.3 14 | python-dateutil==1.5 15 | python-twitter==2.0 16 | pytz==2014.10 17 | requests==2.5.0 18 | requests-oauthlib==0.4.2 19 | simplejson==3.3.3 20 | wsgiref==0.1.2 21 | -------------------------------------------------------------------------------- /run_app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for running the dcmetrometrics worker app on digital ocean 4 | ROOT=/home/lmendelo/dcmetrometrics 5 | source $ROOT/env.sh 6 | source $ROOT/python/virtenv/bin/activate 7 | cd $ROOT/repo 8 | 9 | # Run gunicorn 10 | python app.py -------------------------------------------------------------------------------- /run_gunicorn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for running gunicorn server on digital ocean. 4 | ROOT=/home/lmendelo/dcmetrometrics 5 | source $ROOT/env.sh 6 | source $ROOT/python/virtenv/bin/activate 7 | cd $ROOT/repo 8 | 9 | # Run gunicorn 10 | gunicorn -w 4 -k gevent dcmetrometrics.web.server:app -------------------------------------------------------------------------------- /serve_gunicorn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Expose the server app for gunicorn in a testing environment 3 | with the appropriate environmental variables. 4 | 5 | To run: 6 | gunicorn -w 4 -k gevent serve_gunicorn:app 7 | """ 8 | 9 | # Local Testing 10 | import test.setup 11 | from gevent import monkey; monkey.patch_all() 12 | from dcmetrometrics.web.server import app -------------------------------------------------------------------------------- /serverApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Run a local instance of the DCMetroMetrics server 4 | for local testing. 5 | """ 6 | 7 | # Local Testing 8 | import test.setup 9 | from gevent import monkey; monkey.patch_all() 10 | from dcmetrometrics.web.server import Server 11 | 12 | print 'Running the server locally....' 13 | serverApp = Server() 14 | serverApp.start() 15 | serverApp.join() 16 | 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | packages = ['dcmetrometrics', 4 | 'dcmetrometrics.common', 5 | 'dcmetrometrics.eles', 6 | 'dcmetrometrics.hotcars', 7 | 'dcmetrometrics.keys', 8 | 'dcmetrometrics.test', 9 | 'dcmetrometrics.third_party', 10 | 'dcmetrometrics.web'] 11 | 12 | setup(name='DC Metro Metrics', 13 | version='1.1', 14 | description='Collecting and sharing public data related to the DC WMATA Metrorail system.', 15 | author='Lee Mendelowitz', 16 | author_email='Lee.Mendelowitz@gmail.com', 17 | url='https://github.com/LeeMendelowitz/DCMetroMetrics', 18 | # Uncomment one or more lines below in the install_requires section 19 | # for the specific client drivers/modules your application needs. 20 | install_requires=[ 'greenlet', 21 | 'gevent', 22 | 'requests', 23 | 'python-dateutil==1.5', 24 | 'oauth2', 25 | 'pymongo', 26 | 'simplejson', 27 | 'httplib2', 28 | 'bottle', 29 | 'mongoengine' 30 | ], 31 | packages = [] 32 | ) 33 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | from . import setup 2 | from .setup import connect 3 | -------------------------------------------------------------------------------- /test/status_group.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import setup 3 | 4 | from dcmetrometrics.eles import models 5 | from dcmetrometrics.eles.StatusGroup import StatusGroup 6 | from dcmetrometrics.common.metroTimes import utcnow, nytz 7 | from datetime import timedelta, datetime 8 | 9 | class TestOutageDays(unittest.TestCase): 10 | 11 | def setUp(self): 12 | 13 | t1 = datetime(2015, 2, 16, 22, tzinfo = nytz) 14 | 15 | # Outage does not cover the 17th since resolved before system open 16 | t2 = datetime(2015, 2, 17, 3, tzinfo = nytz) 17 | t3 = datetime(2015, 2, 21, 4, tzinfo = nytz) # This covers 20 and 21 18 | 19 | s1 = models.UnitStatus(time = t1, end_time = t2, symptom_category = "BROKEN") 20 | s2 = models.UnitStatus(time = t2, end_time = t3, symptom_category = "ON" ) 21 | s3 = models.UnitStatus(time = t3, end_time = None, symptom_category = "BROKEN") 22 | 23 | self.statuses = [s1, s2, s3] 24 | self.start_time = t1 25 | 26 | def test_end_time_1(self): 27 | end_time = datetime(2015, 2, 22, 10, tzinfo = nytz) 28 | sg = StatusGroup(self.statuses, self.start_time, end_time) 29 | bd = sg.break_days 30 | self.assertEqual(len(sg.break_days), 4) 31 | 32 | def test_end_time_2(self): 33 | end_time = datetime(2015, 2, 22, 4, tzinfo = nytz) 34 | sg = StatusGroup(self.statuses, self.start_time, end_time) 35 | bd = sg.break_days 36 | self.assertEqual(len(sg.break_days), 3) 37 | 38 | class TestTrimming(unittest.TestCase): 39 | 40 | def setUp(self): 41 | 42 | t1 = datetime(2015, 2, 16, 22, tzinfo = nytz) 43 | 44 | # Outage does not cover the 17th since resolved before system open 45 | t2 = datetime(2015, 2, 17, 3, tzinfo = nytz) 46 | t3 = datetime(2015, 2, 21, 4, tzinfo = nytz) # This covers 20 and 21 47 | 48 | s1 = models.UnitStatus(time = t1, end_time = t2, symptom_category = "BROKEN") 49 | s2 = models.UnitStatus(time = t2, end_time = t3, symptom_category = "ON" ) 50 | s3 = models.UnitStatus(time = t3, end_time = None, symptom_category = "BROKEN") 51 | 52 | self.statuses = [s1, s2, s3] 53 | self.start_time = t1 54 | 55 | def test_trim_1(self): 56 | start_time = datetime(2015, 2, 17, 1, tzinfo = nytz) 57 | end_time = datetime(2015, 2, 25, 4, tzinfo = nytz) 58 | statuses = self.statuses 59 | sg = StatusGroup(statuses, start_time, end_time) 60 | self.assertEqual(sg.statuses_trimmed[0].time, start_time) 61 | self.assertEqual(sg.statuses_trimmed[-1].end_time, end_time) 62 | 63 | def test_trim_2(self): 64 | start_time = datetime(2015, 2, 17, 1, tzinfo = nytz) 65 | end_time = datetime(2015, 2, 21, 2, tzinfo = nytz) 66 | statuses = self.statuses 67 | sg = StatusGroup(statuses, start_time, end_time) 68 | self.assertEqual(sg.statuses_trimmed[0].time, start_time) 69 | self.assertEqual(sg.statuses_trimmed[-1].end_time, end_time) 70 | 71 | 72 | 73 | if __name__ == '__main__': 74 | unittest.main() -------------------------------------------------------------------------------- /test/test_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Test the app.py in the repo directory 4 | This includes the hotcars app, and the escalator app 5 | """ 6 | 7 | import os, sys, subprocess, imp 8 | 9 | # Setup environmental variables and fix the system path 10 | import setup 11 | 12 | from gevent import monkey; monkey.patch_all() 13 | appPath = os.path.join(setup.HOME_DIR, 'app.py') 14 | appModule = imp.load_source('app', appPath) 15 | appModule.run() 16 | -------------------------------------------------------------------------------- /test/test_exportDB.py: -------------------------------------------------------------------------------- 1 | import setup 2 | import utils.exportDB 3 | utils.exportDB.run() 4 | -------------------------------------------------------------------------------- /test/test_exportdata.py: -------------------------------------------------------------------------------- 1 | import setup 2 | import utils.exportData 3 | utils.exportData.run() 4 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import isOpenshiftEnv, fixSysPath 2 | -------------------------------------------------------------------------------- /utils/fixHotCarColors.py: -------------------------------------------------------------------------------- 1 | # Functions to help manually add a line color to hot car 2 | # reports which are missing a line color: 3 | 4 | # dumpCSV: Dump a CSV file with hot car reports which are missing line color 5 | # addColorsFromCSV: Read in colors from a csv file. 6 | # genUpdateScript: Generate a python script to update the MongoDB hotcars collection 7 | ##################################################### 8 | #from . import test.test_setup 9 | from . import dbUtils 10 | from . import hotCars 11 | 12 | import pandas 13 | import bson 14 | from StringIO import StringIO 15 | 16 | db = dbUtils.getDB() 17 | 18 | def dumpCSV(fname = 'hotcars.colormissing.csv'): 19 | hotCarDict = hotCars.getAllHotCarReports(db) 20 | allReports = [v for vl in hotCarDict.values() for v in vl] 21 | missingColor = [h for h in allReports if h['color']=='NONE'] 22 | print '%i reports are missing color'%len(missingColor) 23 | 24 | myFields = ['_id', 'text', 'color'] 25 | data = [] 26 | for h in missingColor: 27 | d = dict((k,h[k]) for k in myFields) 28 | d['_id'] = str(d['_id']) 29 | d['text'] = d['text'].encode('ascii', errors='ignore').replace(',',' ') 30 | data.append(d) 31 | dt = pandas.DataFrame(data) 32 | dt.to_csv(fname,index=False) 33 | 34 | def addColorsFromCSV(fname): 35 | dt = pandas.read_csv(fname) 36 | numRec = len(dt) 37 | colorInd = dt['color'] != 'NONE' 38 | colorDt = dt[colorInd] 39 | numColor = len(colorDt) 40 | print 'Found %i records out of %i with color'%(numColor, numRec) 41 | for recId, d in colorDt.iterrows(): 42 | myId = d['_id'] 43 | color = d['color'] 44 | print myId, color 45 | myId = bson.objectid.ObjectId(myId) 46 | db.hotcars.update({"_id" : myId}, {"$set" : {"color" : color}}) 47 | 48 | script =\ 49 | """\ 50 | #!/usr/bin/env python 51 | import bson, os, sys 52 | 53 | if "OPENSHIFT_MONGODB_DB_HOST" not in os.environ: 54 | print "Seems like this is not the OPENSHIFT environment. Importing test_setup." 55 | import test_setup 56 | 57 | import dbUtils 58 | db = dbUtils.getDB() 59 | 60 | {cmds} 61 | 62 | """ 63 | 64 | 65 | def genUpdateScript(csvname, output='updateDBWithColors.py'): 66 | dt = pandas.read_csv(csvname) 67 | numRec = len(dt) 68 | colorInd = dt['color'] != 'NONE' 69 | colorDt = dt[colorInd] 70 | numColor = len(colorDt) 71 | print 'Found %i records out of %i with color'%(numColor, numRec) 72 | cmds = StringIO() 73 | for recId, d in colorDt.iterrows(): 74 | myId = d['_id'] 75 | color = d['color'] 76 | print myId, color 77 | cmd = 'db.hotcars.update({{"_id" : bson.objectid.ObjectId("{0}")}}, {{"$set" : {{"color" : "{1}"}}}})'.format(myId,color) 78 | cmds.write(cmd + "\n") 79 | myScript = script.format(cmds=cmds.getvalue()) 80 | cmds.close() 81 | fout = open(output, 'w') 82 | fout.write(myScript) 83 | -------------------------------------------------------------------------------- /utils/generateAllWebPages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generate all web pages. 4 | This script works for both an OpenShift deployment 5 | and for local testing. 6 | """ 7 | 8 | import sys, os 9 | import utils 10 | 11 | if not utils.isOpenshiftEnv(): 12 | print 'This does not appear to be an Openshift Environment.' 13 | utils.fixSysPath() 14 | import test.setup # Fixes the system path so we can import dcmetrometrics. 15 | else: 16 | print 'This appears to be an Openshift Environment.' 17 | 18 | print '*'*50 19 | 20 | from dcmetrometrics.web import WebPageGenerator 21 | WebPageGenerator.updateAllPages() 22 | 23 | 24 | -------------------------------------------------------------------------------- /utils/generate_site_map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generate a sitemap.xml file for dc metro metrics 4 | and a json file with an array of urls. 5 | 6 | The json file is used with grunt in order to crawl dcmetrometrics.com and regenerate 7 | the static version of the site. 8 | """ 9 | 10 | from dcmetrometrics.eles import models as models_eles 11 | from dcmetrometrics.hotcars import models as models_hotcars 12 | import sys 13 | 14 | ####################### 15 | # Set up logging 16 | import logging 17 | sh = logging.StreamHandler(sys.stderr) 18 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 19 | logger = logging.getLogger('SiteMapGenerator') 20 | logger.setLevel(logging.DEBUG) 21 | logger.addHandler(sh) 22 | ####################### 23 | 24 | URL_ROOT = 'http://localhost:80' 25 | 26 | def unit_urls(): 27 | logger.info("Generating Unit URLS...") 28 | units = models_eles.Unit.objects 29 | return ['{URL_ROOT}/unit/%s'%(unit.unit_id) for unit in units] 30 | 31 | def hotcar_urls(): 32 | logger.info("Generating HotCar URLS...") 33 | hotcars = models_hotcars.HotCarReport.objects 34 | return list(set(['{URL_ROOT}/hotcars/detail/%s'%(h.car_number) for h in hotcars])) 35 | 36 | def station_urls(): 37 | logger.info("Generating Station URLS...") 38 | stations = models_eles.Station.objects 39 | short_names = set(s.short_name for s in stations) 40 | return ['{URL_ROOT}/stations/detail/%s'%(s) for s in short_names] 41 | 42 | def main_urls(): 43 | logger.info("Generating Main URLS...") 44 | pages = ['home', 'stations/list', 'about', 'outages', 'rankings', ''] 45 | return ['{URL_ROOT}/%s'%(p) for p in pages] 46 | 47 | URLS = unit_urls() + hotcar_urls() + station_urls() + main_urls() 48 | 49 | 50 | def make_site_map(output_file_name, URL_ROOT = URL_ROOT): 51 | import xml.etree.ElementTree as ET 52 | urls = [u.format(URL_ROOT = URL_ROOT) for u in URLS] 53 | urlset_node = ET.Element('urlset') 54 | urlset_node.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9') 55 | tree = ET.ElementTree(urlset_node) 56 | 57 | for url in urls: 58 | url_node = ET.SubElement(urlset_node, 'url') 59 | loc_node = ET.SubElement(url_node, 'loc') 60 | loc_node.text = url 61 | changefreq_node = ET.SubElement(url_node, 'changefreq') 62 | changefreq_node.text = 'daily' 63 | 64 | with open(output_file_name, 'w') as fout: 65 | tree.write(fout, encoding="utf-8", xml_declaration = True) 66 | 67 | def write_url_json(output_file_name, URL_ROOT = URL_ROOT): 68 | import json 69 | urls = [u.format(URL_ROOT = URL_ROOT) for u in URLS] 70 | with open(output_file_name, 'w') as fout: 71 | json.dump(urls, fout) 72 | 73 | if __name__ == "__main__": 74 | #make_site_map('sitemap.xml', URL_ROOT='http://www.dcmetrometrics.com') 75 | write_url_json('site_urls.json', URL_ROOT='') 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /utils/importData.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Downloads the latest DC Metro Metrics data from www.DCMetroMetrics.com 4 | Loads the data into the local MongoDB Database 5 | An instance of mongod must be running. 6 | """ 7 | 8 | from pymongo import MongoClient 9 | import os, sys, shutil, glob, argparse 10 | from subprocess import call 11 | 12 | parser = argparse.ArgumentParser(description="Import DCMetroMetrics data into MongoDB") 13 | parser.add_argument('--db', default="MetroEscalators", help='The database to use.') 14 | parser.add_argument('--zip', default="./DCMetroMetricsData.zip", help='The zip file to use') 15 | 16 | c = MongoClient() 17 | 18 | 19 | def run(): 20 | args = parser.parse_args() 21 | 22 | ZIP = args.zip 23 | 24 | if not os.path.exists(ZIP): 25 | raise RuntimeError("Could not find zip file: %s"%ZIP) 26 | 27 | UNZIPPED_DIR = os.path.abspath('./DCMetroMetricsData') 28 | 29 | # Download the latest data zip file 30 | # cmd = "curl http://www.dcmetrometrics.com/data/{ZIP} > {ZIP}".format(ZIP=ZIP) 31 | # call(cmd, shell=True) 32 | 33 | if os.path.exists(UNZIPPED_DIR): 34 | shutil.rmtree(UNZIPPED_DIR) 35 | 36 | # Unzip 37 | cmd = "unzip %s"%ZIP 38 | call(cmd, shell=True) 39 | 40 | db = c[args.db] 41 | 42 | jsons = glob.glob('%s/*.json'%UNZIPPED_DIR) 43 | print 'Json Directory: %s'%UNZIPPED_DIR 44 | print 'Found %i jsons'%len(jsons) 45 | for j in jsons: 46 | path, name = os.path.split(j) 47 | print ' - %s'%(name) 48 | 49 | for j in jsons: 50 | path, fname = os.path.split(j) 51 | base, ext = os.path.splitext(fname) 52 | collection = base 53 | print '*'*50 54 | print 'Dropping collection %s'%collection 55 | db[collection].drop() 56 | print 'Importing json %s into collection %s'%(j, collection) 57 | cmd = 'mongoimport --db {db} --collection {collection} {fname}' 58 | cmd = cmd.format(db = args.db, collection=collection, fname=j) 59 | call(cmd, shell=True) 60 | 61 | if __name__ == '__main__': 62 | run() 63 | -------------------------------------------------------------------------------- /utils/migration.py: -------------------------------------------------------------------------------- 1 | """Migrate the database to the new version. 8/11/2014""" 2 | 3 | import test 4 | from dcmetrometrics.common import dbGlobals 5 | dbGlobals.connect() 6 | 7 | from utils import denormalize_db 8 | denormalize_db.update_symptom_codes() 9 | denormalize_db.update_unit_statuses() 10 | denormalize_db.denormalize_unit_statuses() 11 | denormalize_db.recompute_key_statuses() 12 | 13 | 14 | # Finally, we need to create the stations in the database 15 | denormalize_db.add_station_docs() -------------------------------------------------------------------------------- /utils/migrations_hotcars.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to perform database migrations for the ELES App. 3 | 4 | Script to denormalize the database by adding fields to the unit status records. 5 | 6 | This should be imported as a module and not run directly. 7 | """ 8 | 9 | from . import utils 10 | utils.fixSysPath() 11 | 12 | import sys 13 | from datetime import datetime 14 | 15 | from dcmetrometrics.common.dbGlobals import G 16 | from dcmetrometrics.hotcars.models import (HotCarAppState, HotCarTweeter, HotCarTweet, 17 | HotCarReport, CarsForbiddenByMention, Temperature) 18 | from dcmetrometrics.hotcars.twitter_api import TwitterError, getTwitterAPI 19 | from datetime import timedelta 20 | import logging 21 | 22 | from dcmetrometrics.common.JSONifier import JSONWriter 23 | from dcmetrometrics.common.globals import WWW_DIR 24 | 25 | ########################## 26 | def update_twitter_handles(): 27 | """ 28 | For each twitter user, look up the user and the handle. 29 | Save the twitter user to the hotcars_tweeters collection. 30 | """ 31 | 32 | user_ids = set(t.user_id for t in HotCarTweeter.objects) 33 | user_ids = list(user_ids) 34 | 35 | print "Have %i users"%len(user_ids) 36 | 37 | # Make requests in batch of 100 38 | def gen_batches(d): 39 | i = 0 40 | n = 100 41 | ret = d[i:i+n] 42 | while ret: 43 | yield ret 44 | i = i + n 45 | ret = d[i:i+n] 46 | 47 | T = getTwitterAPI() 48 | 49 | for i, user_id_batch in enumerate(gen_batches(user_ids)): 50 | 51 | print "Querying Twitter with batch %i"%i 52 | 53 | try: 54 | 55 | user_info = T.UsersLookup(user_id = user_id_batch) 56 | 57 | for user in user_info: 58 | user_id = user.id 59 | handle = user.screen_name 60 | HotCarTweeter.update(user_id, handle) 61 | 62 | except TwitterError as e: 63 | print("Caught twitter error:\n%s"%str(e)) 64 | 65 | denormalize_reports() 66 | 67 | def denormalize_reports(): 68 | """ 69 | Denormalize all hot car report docs by setting the 70 | user_id field, text field, and handle field. 71 | 72 | """ 73 | for doc in HotCarReport.iter_reports(): 74 | doc.denormalize() 75 | 76 | def write_json(): 77 | jwriter = JSONWriter(WWW_DIR) 78 | jwriter.write_hotcars() 79 | jwriter.write_hotcars_by_day() -------------------------------------------------------------------------------- /utils/trimLogs.py: -------------------------------------------------------------------------------- 1 | import glob, sys, os 2 | import subprocess 3 | import shutil 4 | 5 | DATA_DIR = os.environ['OPENSHIFT_DATA_DIR'] 6 | os.chdir(DATA_DIR) 7 | 8 | logFiles = glob.glob('*.log') 9 | logFiles = [os.path.abspath(l) for l in logFiles] 10 | 11 | print 'Found %i log files'%len(logFiles) 12 | print '\n'.join(logFiles) 13 | 14 | def trimLog(logFile, numLines = 200000): 15 | path, fn = os.path.split(logFile) 16 | bn, ext = os.path.splitext(fn) 17 | tempFile = '%s.temp'%fn 18 | tempFile = os.path.join(path, tempFile) 19 | 20 | # Execute tail 21 | cmd = ['tail', '-n', str(numLines), logFile] 22 | out = open(tempFile, 'w') 23 | subprocess.call(cmd, stdout = out) 24 | out.close() 25 | 26 | # Remove original log file 27 | os.remove(logFile) 28 | 29 | # Copy the shortened log file 30 | os.rename(tempFile, logFile) 31 | 32 | for logFile in logFiles: 33 | logFile = os.path.abspath(logFile) 34 | trimLog(logFile) 35 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | _this_file = os.path.abspath(__file__) 4 | _this_dir = os.path.split(_this_file)[0] 5 | ROOT_DIR = os.path.split(_this_dir)[0] 6 | 7 | def isOpenshiftEnv(): 8 | """ 9 | Return True if this appears to be an Openshift Environment. 10 | """ 11 | MIN_ENV_COUNT = 30 12 | env_vars = os.environ.keys() 13 | isOpenshiftEnv = lambda s: 'OPENSHIFT' == s[:9] 14 | openshift_envs = [e for e in env_vars if isOpenshiftEnv(e)] 15 | return len(openshift_envs) >= MIN_ENV_COUNT 16 | 17 | def fixSysPath(): 18 | if ROOT_DIR not in sys.path: 19 | sys.path = [ROOT_DIR] + sys.path 20 | 21 | fixSysPath() 22 | -------------------------------------------------------------------------------- /webPageGeneratorApp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run an instance of the WebPageGenerator restartingGreenlet 3 | for local testing. 4 | """ 5 | 6 | import test.setup 7 | from dcmetrometrics.web.WebPageGenerator import WebPageGenerator 8 | print 'Running the webPageGenerator locally' 9 | app = WebPageGenerator() 10 | app.start() 11 | app.join() 12 | -------------------------------------------------------------------------------- /wsgi/static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/.gitignore -------------------------------------------------------------------------------- /wsgi/static/architect.logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/architect.logo.png -------------------------------------------------------------------------------- /wsgi/static/dailycaller.logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/dailycaller.logo.png -------------------------------------------------------------------------------- /wsgi/static/dcist.logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/dcist.logo.gif -------------------------------------------------------------------------------- /wsgi/static/hotair.logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/hotair.logo.gif -------------------------------------------------------------------------------- /wsgi/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | padding-bottom: 40px; 4 | background-color:#D0D0D0; 5 | background: url('/static/wmata_ceiling_banner_web.jpg') no-repeat center center fixed; 6 | -webkit-background-size: cover; 7 | -moz-background-size: cover; 8 | -o-background-size: cover; 9 | background-size: cover; 10 | } 11 | 12 | @media (max-width:979px){ 13 | body{ 14 | padding-top:0px; 15 | padding-bottom:0px; 16 | } 17 | } 18 | 19 | .footer { 20 | margin-top: 10px; 21 | padding-top: 10px; 22 | background-color: white; 23 | border-radius: 6px 6px 6px 6px; 24 | } 25 | 26 | .navbar .nav > li > .dropdown-menu:after { 27 | border: none; 28 | } 29 | 30 | .googletable td{border: thin solid #dddddd;} 31 | .googletable td.success{background-color:#dff0d8;} 32 | .googletable td.error{background-color:#f2dede;} 33 | .googletable td.warning{background-color:#fcf8e3;} 34 | .googletable td.info{background-color:#d9edf7;} 35 | 36 | 37 | .main-content { 38 | padding: 25px; 39 | background-color: white; 40 | border-radius: 6px 6px 6px 6px; 41 | } 42 | 43 | 44 | .press-item .publisher { 45 | color: blue; 46 | } 47 | 48 | .updateTime 49 | { 50 | padding-top:20px; 51 | color:black 52 | font-size:small; 53 | text-align:right; 54 | } 55 | 56 | .redsquare 57 | { 58 | width:14px; 59 | height:14px; 60 | background-color:red; 61 | border:1px solid black; 62 | float:left; 63 | margin:5px; 64 | } 65 | 66 | .bluesquare 67 | { 68 | width:14px; 69 | height:14px; 70 | background-color:blue; 71 | border:1px solid black; 72 | float:left; 73 | margin:5px; 74 | } 75 | .yellowsquare 76 | { 77 | width:14px; 78 | height:14px; 79 | background-color:yellow; 80 | border:1px solid black; 81 | float:left; 82 | margin:5px; 83 | } 84 | .orangesquare 85 | { 86 | width:14px; 87 | height:14px; 88 | background-color:orange; 89 | border:1px solid black; 90 | float:left; 91 | margin:5px; 92 | } 93 | .greensquare 94 | { 95 | width:14px; 96 | height:14px; 97 | background-color:green; 98 | border:1px solid black; 99 | float:left; 100 | margin:5px; 101 | } -------------------------------------------------------------------------------- /wsgi/static/unsuckdcmetro.logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/unsuckdcmetro.logo.jpg -------------------------------------------------------------------------------- /wsgi/static/wapo.logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/wapo.logo.png -------------------------------------------------------------------------------- /wsgi/static/wmata-gallery-place.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/wmata-gallery-place.jpg -------------------------------------------------------------------------------- /wsgi/static/wmata_ceiling_banner_blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/wmata_ceiling_banner_blue.jpg -------------------------------------------------------------------------------- /wsgi/static/wmata_ceiling_banner_web.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/wmata_ceiling_banner_web.jpg -------------------------------------------------------------------------------- /wsgi/static/wusa9logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeMendelowitz/DCMetroMetrics/73b28117fbdb35886cec0e05328a19416550c6a7/wsgi/static/wusa9logo.gif -------------------------------------------------------------------------------- /wsgi/views/DCMetroMetricRedesign/rankingsContent.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

Rankings

6 | 7 |

Explore which escalators are the most broken, and which stations have the 8 | best and worst performing escalators. Also explore historical daily counts 9 | of escalator breaks and inspections.

10 | 11 |

12 | Escalator Rankings  13 | Station Rankings  14 | Trends  15 |

16 | 17 | 18 | 19 |

Escalator Rankings (top)

20 |

21 |

Glossary

22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 | 1d  30 | 3d  31 | 7d  32 | 14d  33 | 28d  34 | All Time

35 |
36 |
37 |
38 | 39 | 40 | 41 |

Station Rankings (top)

42 | Glossary 43 |
44 | 45 | 46 |

Trends (top)

47 |

Number of breaks (red) and inspections (blue) per day.

48 |
49 |

Note: "Data outages" refer to times when escalator data was not 50 | collected due to technical difficulties, resulting in low counts.

51 | 52 | 53 |
54 |

Page Last Updated: 07/16/13 06:36 PM

55 |
56 | 57 | 58 | 59 | 60 |
61 | -------------------------------------------------------------------------------- /wsgi/views/data.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Data Downloads

5 | 6 |

From this page you can download all of the data used to 7 | generate this website. All files are updated daily.

8 | 9 |

License

10 | 11 |

All DC Metro Metrics data is made available as open data 12 | under the 13 | 14 | Open Database License (ODbL) v1.0. 15 | 16 |

In summary, you are free to:

17 |

18 |

23 |

24 |

provided that:

25 |

26 |

31 |

32 | 33 |

Please see the license for full details.

34 | 35 |

HotCars Data

36 | 37 |

In accordance with the 38 | 39 | Twitter API terms of service, 40 | this dataset only include tweet ID's and Twitter user ID's.

41 | 42 |

HotCars Data (JSON)

43 |

HotCars Data (CSV)

44 | 45 |

Escalator Data

46 | 47 |

All escalator data was obtained in real-time through the WMATA API. 48 | 49 |

Escalator descriptions and statuses (JSON)

50 |

Escalator descriptions (CSV)

51 |

Escalator statuses (CSV)

52 | 53 |
54 |
55 | 56 | 57 | %rebase layout title='DC Metro Metrics: Data Downloads' -------------------------------------------------------------------------------- /wsgi/views/elevator.tpl: -------------------------------------------------------------------------------- 1 | %# Listing for an escalator 2 | 3 | 4 | %from dcmetrometrics.web.eles import stationCodeToWebPath, symptomCategoryToClass 5 | %from dcmetrometrics.common.metroTimes import secondsToDHM, secondsToHMS, toLocalTime 6 | 7 | %description = "DC Metro Elevator {0} at {1} station, {2} {3}. Elevator performance history, and more." 8 | %description = description.format(unitId, escData['station_name'], escData['station_desc'], escData['esc_desc']) 9 | %#escalator data 10 | 11 |
12 |
13 | 14 |

Elevator {{unitId}}

15 | 16 | %stationWebPath = stationCodeToWebPath(escData['station_code']) 17 | 18 | 19 | 20 | 21 |
Station Name{{escData['station_name']}}
Station Code{{escData['station_code']}}
Station Description{{escData['station_desc']}}
Elevator Description{{escData['esc_desc']}}
22 | 23 | %#escalator summary 24 |

Summary

25 | 26 | 27 | 28 | 29 | 30 | 31 | %# 32 |
Num. Breaks{{escSummary['numBreaks']}}
Num. Fixes{{escSummary['numFixes']}}
Num. Inspections{{escSummary['numInspections']}}
Availability{{'%.3f%%'%(100.0*escSummary['availability'])}}
Report Time Range{{secondsToDHM(escSummary['absTime'])}}
Report Metro Open Time{{secondsToDHM(escSummary['metroOpenTime'])}}
33 | 34 |

History

35 | 36 | 37 | 38 | 39 | 40 | %# 41 | 42 | %for status in statuses: 43 | % tf = '%a, %m/%d/%y %I:%M %p' 44 | % localTime = toLocalTime(status['time']) 45 | % timeStr = localTime.strftime(tf) 46 | % durationStr = '' 47 | % if 'end_time' in status: 48 | % duration = status['end_time'] - status['time'] 49 | % durationStr = secondsToDHM(duration.total_seconds()) 50 | % end 51 | % tickDeltaStr = secondsToHMS(status.get('tickDelta',0.0)) 52 | % symptomCat = status['symptomCategory'].lower() 53 | 54 | 55 | 56 | 57 | %# 58 | 59 | %end 60 |
TimeStatusDurationTick Delta
{{timeStr}}{{status['symptom']}}{{durationStr}}{{tickDeltaStr}}
61 | 62 | %tf = '%m/%d/%y %I:%M %p' 63 | %updateStr = toLocalTime(curTime).strftime(tf) 64 |
65 |

Page Last Updated: {{updateStr}}

66 |
67 | 68 |
69 |
70 | 71 | %rebase layout title=unitId, description=description 72 | -------------------------------------------------------------------------------- /wsgi/views/elevators.tpl: -------------------------------------------------------------------------------- 1 | %# Page for a station 2 | %from dcmetrometrics.web import eles as metroEscalatorsWeb 3 | %from dcmetrometrics.web.eles import lineToColoredSquares, eleUnitIdToWebPath, stationCodeToWebPath, symptomCategoryToClass 4 | %from dcmetrometrics.common.metroTimes import toLocalTime 5 | 6 |
7 |
8 | 9 |

Elevators

10 | 11 | %stationNames = sorted(stationToEsc.keys()) 12 | %stationLetters = set(sn[0].upper() for sn in stationNames) 13 | %seenLetters = set() 14 | 15 | %#Create a list of bookmark links by letter 16 | % for letter in sorted(stationLetters): 17 | {{letter}} 18 | % end 19 | 20 | %for station in stationNames: 21 | % escList = stationToEsc[station] 22 | % if not escList: 23 | % continue 24 | % end 25 | % stationCode = escList[0]['stationCode'] 26 | % stationWebPath = stationCodeToWebPath(stationCode) 27 | % hasEntranceData = any(esc['stationDesc'] for esc in escList) 28 | % firstLetter = station[0].upper() 29 | % sawFirstLetter = firstLetter in seenLetters 30 | % if not sawFirstLetter: 31 | % seenLetters.add(firstLetter) 32 | % end 33 |
34 | % if sawFirstLetter: 35 |

{{station}}

36 | % else: 37 | % #Seeing this letter for first time, add a bookmark 38 | 39 |

{{station}}

40 | 41 | % end 42 | 43 | 44 | 45 | 46 | % if hasEntranceData: 47 | 48 | % end 49 | 50 | 51 | 52 | % for esc in escList: 53 | % escWebPath = eleUnitIdToWebPath(esc['unitId']) 54 | % symptomCat = esc['symptomCategory'].lower() 55 | 56 | 57 | % if hasEntranceData: 58 | 59 | % end 60 | 61 | 62 | %# 63 | 64 | % end 65 |
UnitEntranceDescriptionStatus
{{esc['unitId']}}{{esc['stationDesc']}}{{esc['escDesc']}}{{esc['symptom']}}{{'%.2f%%'%(100.0*esc['availability'])}}
66 |
67 | %end 68 | 69 | %tf = '%m/%d/%y %I:%M %p' 70 | %updateStr = toLocalTime(curTime).strftime(tf) 71 |
72 |

Page Last Updated: {{updateStr}}

73 |
74 | 75 |
76 |
77 | 78 | %rebase layout title='DC Metro Metrics: All Elevators', description='' 79 | -------------------------------------------------------------------------------- /wsgi/views/escalator.tpl: -------------------------------------------------------------------------------- 1 | %# Listing for an escalator 2 | 3 | %from dcmetrometrics.web.eles import stationCodeToWebPath, symptomCategoryToClass 4 | %from dcmetrometrics.common.metroTimes import secondsToDHM, secondsToHMS, toLocalTime 5 | 6 | %description = "DC Metro Escalator {0} at {1} station, {2} {3}. Escalator performance history, and more." 7 | %description = description.format(unitId, escData['station_name'], escData['station_desc'], escData['esc_desc']) 8 | %#escalator data 9 | 10 |
11 |
12 | 13 |

Escalator {{unitId}}

14 | 15 | %stationWebPath = stationCodeToWebPath(escData['station_code']) 16 | 17 | 18 | 19 | 20 |
Station Name{{escData['station_name']}}
Station Code{{escData['station_code']}}
Station Description{{escData['station_desc']}}
Escalator Description{{escData['esc_desc']}}
21 | 22 | %#escalator summary 23 |

Summary

24 | 25 | 26 | 27 | 28 | 29 | 30 | %# 31 |
Num. Breaks{{escSummary['numBreaks']}}
Num. Fixes{{escSummary['numFixes']}}
Num. Inspections{{escSummary['numInspections']}}
Availability{{'%.3f%%'%(100.0*escSummary['availability'])}}
Report Time Range{{secondsToDHM(escSummary['absTime'])}}
Report Metro Open Time{{secondsToDHM(escSummary['metroOpenTime'])}}
32 | 33 |

History

34 | 35 | 36 | 37 | 38 | 39 | %# 40 | 41 | %for status in statuses: 42 | % tf = '%a, %m/%d/%y %I:%M %p' 43 | % localTime = toLocalTime(status['time']) 44 | % timeStr = localTime.strftime(tf) 45 | % durationStr = '' 46 | % if 'end_time' in status: 47 | % duration = status['end_time'] - status['time'] 48 | % durationStr = secondsToDHM(duration.total_seconds()) 49 | % end 50 | % tickDeltaStr = secondsToHMS(status.get('tickDelta',0.0)) 51 | % symptomCat = status['symptomCategory'].lower() 52 | 53 | 54 | 55 | 56 | %# 57 | 58 | %end 59 |
TimeStatusDurationTick Delta
{{timeStr}}{{status['symptom']}}{{durationStr}}{{tickDeltaStr}}
60 | 61 | %tf = '%m/%d/%y %I:%M %p' 62 | %updateStr = toLocalTime(curTime).strftime(tf) 63 |
64 |

Page Last Updated: {{updateStr}}

65 |
66 | 67 |
68 |
69 | 70 | %rebase layout title=unitId, description=description 71 | -------------------------------------------------------------------------------- /wsgi/views/escalatorOutages.tpl: -------------------------------------------------------------------------------- 1 | %# Page for escalator outages 2 | 3 | %from dcmetrometrics.web import eles as metroEscalatorsWeb 4 | %from dcmetrometrics.web.eles import lineToColoredSquares, escUnitIdToWebPath, stationCodeToWebPath 5 | %from dcmetrometrics.common.metroTimes import toLocalTime 6 | 7 |
8 |
9 | 10 |

Escalator Outages

11 | 12 |

Summary

13 | 14 | 15 | 16 |
Availability{{'%.2f%%'%(100.0*systemAvailability['availability'])}}
Weighted Availability{{'%.2f%%'%(100.0*systemAvailability['weightedAvailability'])}}
17 | 18 |

Symptom Counts

19 | 20 |
21 | 22 | %totalCount = sum(symptomCounts.itervalues()) 23 | %for i,(symptom,count) in enumerate(symptomCounts.most_common()): 24 | 25 | %end 26 | 27 |
{{symptom}}{{'%i'%(count)}}
TOTAL{{'%i'%totalCount}}
28 |
29 | 30 |
31 |
32 | 33 | 34 |

Outages

35 | 36 |
37 | 38 | %hasEntranceData = any(esc['stationDesc'] for esc in escList) 39 |
40 | 41 | 42 | %if hasEntranceData: 43 | 44 | 45 | 46 | 47 | %else: 48 | 49 | 50 | 51 | %end 52 | 53 | %for esc in escList: 54 | 55 | 56 | 57 | % if hasEntranceData: 58 | 59 | % end 60 | 61 | %# 62 | 63 | %end 64 |
UnitStationEntranceStatusUnitStationStatus
{{!esc['unitIdHtml']}}{{!esc['stationNameHtml']}}{{esc['stationDesc']}}{{esc['symptom']}}{{'%.2f%%'%(100.0*esc['availability'])}}
65 |
66 | 67 | %tf = '%m/%d/%y %I:%M %p' 68 | %updateStr = toLocalTime(curTime).strftime(tf) 69 |
70 |

Page Last Updated: {{updateStr}}

71 |
72 | 73 |
74 |
75 | 76 | %def scriptsToInclude(): 77 | 78 | 79 | %end 80 | 81 | %rebase layout title='DC Metro Metrics: Nonoperational Escalators', scriptsToInclude=scriptsToInclude 82 | -------------------------------------------------------------------------------- /wsgi/views/escalators.tpl: -------------------------------------------------------------------------------- 1 | %# Page for a station 2 | %from dcmetrometrics.web import eles as metroEscalatorsWeb 3 | %from dcmetrometrics.web.eles import lineToColoredSquares, escUnitIdToWebPath, stationCodeToWebPath, symptomCategoryToClass 4 | %from dcmetrometrics.common.metroTimes import toLocalTime 5 | 6 |
7 |
8 | 9 |

Escalators

10 | 11 | %stationNames = sorted(stationToEsc.keys()) 12 | %stationLetters = set(sn[0].upper() for sn in stationNames) 13 | %seenLetters = set() 14 | 15 | %#Create a list of bookmark links by letter 16 | % for letter in sorted(stationLetters): 17 | {{letter}} 18 | % end 19 | 20 | %for station in stationNames: 21 | % escList = stationToEsc[station] 22 | % if not escList: 23 | % continue 24 | % end 25 | % stationCode = escList[0]['stationCode'] 26 | % stationWebPath = stationCodeToWebPath(stationCode) 27 | % hasEntranceData = any(esc['stationDesc'] for esc in escList) 28 | % firstLetter = station[0].upper() 29 | % sawFirstLetter = firstLetter in seenLetters 30 | % if not sawFirstLetter: 31 | % seenLetters.add(firstLetter) 32 | % end 33 |
34 | % if sawFirstLetter: 35 |

{{station}}

36 | % else: 37 | % #Seeing this letter for first time, add a bookmark 38 | 39 |

{{station}}

40 | 41 | % end 42 | 43 | 44 | 45 | 46 | % if hasEntranceData: 47 | 48 | % end 49 | 50 | 51 | 52 | % for esc in escList: 53 | % escWebPath = escUnitIdToWebPath(esc['unitId']) 54 | % symptomCat = esc['symptomCategory'].lower() 55 | 56 | 57 | % if hasEntranceData: 58 | 59 | % end 60 | 61 | 62 | %# 63 | 64 | % end 65 |
UnitEntranceDescriptionStatus
{{esc['unitId']}}{{esc['stationDesc']}}{{esc['escDesc']}}{{esc['symptom']}}{{'%.2f%%'%(100.0*esc['availability'])}}
66 |
67 | %end 68 | 69 | %tf = '%m/%d/%y %I:%M %p' 70 | %updateStr = toLocalTime(curTime).strftime(tf) 71 |
72 |

Page Last Updated: {{updateStr}}

73 |
74 | 75 |
76 |
77 | 78 | %rebase layout title='DC Metro Metrics: All Escalators', description='' 79 | -------------------------------------------------------------------------------- /wsgi/views/header.tpl: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /wsgi/views/home.tpl: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |

DC Metro Metrics

6 |
7 |
8 |

DC Metro Metrics is a project dedicated to collecting and sharing publicly available data related to the DC Metrorail system.

9 | 10 |

This website compiles data which is not available anywhere else, including:

11 | 18 | 19 |

This website and its features are a work in progress. 20 | If you have any questions or comments, or would like to get involved, 21 | please contact me at info@dcmetrometrics.com.

22 | 23 |

Thanks for visiting!

24 | 25 |
26 |
27 | 28 | %rebase layout title='', description='' 29 | -------------------------------------------------------------------------------- /wsgi/views/hotCar.tpl: -------------------------------------------------------------------------------- 1 | %# Hot Car Page 2 | %from dcmetrometrics.web.hotcars import formatTimeStr 3 | %from dcmetrometrics.web.eles import lineToColoredSquares 4 | %from dcmetrometrics.common.metroTimes import toLocalTime 5 | 6 | %description = "Reports for #wmata #hotcar {0} of the WMATA Metrorail System.".format(carNum) 7 | 8 |
9 |
10 | 11 |

HotCar {{carNum}}

12 | 13 |

Summary

14 | 15 | %lineStr = lineToColoredSquares(colors) 16 | 17 | 18 | 19 | 20 |
Num. Reports{{numReports}}
Lines{{!lineStr}}
Last Report Time{{lastReportTimeStr}}
21 | 22 |

Reports

23 | 24 |
25 | %for report in reports: 26 |
27 | {{!formatTimeStr(report['time'])}} 28 | {{!report['embed_html']}} 29 |
30 |
31 | %end 32 |
33 | 34 | %tf = '%m/%d/%y %I:%M %p' 35 | %updateStr = toLocalTime(curTime).strftime(tf) 36 |
37 |

Page Last Updated: {{updateStr}}

38 |
39 | 40 |
41 |
42 | 43 | %rebase layout title='Hot Car %i'%carNum, description=description 44 | -------------------------------------------------------------------------------- /wsgi/views/left_column.tpl: -------------------------------------------------------------------------------- 1 | %from metroEscalatorsWeb import PATHS 2 | 29 | -------------------------------------------------------------------------------- /wsgi/views/old/allHotCars.tpl: -------------------------------------------------------------------------------- 1 | %# List of all hot cars 2 | % import hotCarsWeb 3 | 4 | 5 | 6 | Metro Hot Cars 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | %sortedKeys = sorted(hotCarDict.keys(), key = lambda k: len(hotCarDict[k]), reverse=True) 21 | %for carNum in sortedKeys: 22 | % records = hotCarDict[carNum] 23 | 24 | 25 | 26 | 27 | %# Disable html character escaping\\ 28 | 29 | 30 | %end 31 |
Car NumberColorNum. ReportsTweet Links
{{carNum}}{{!hotCarsWeb.makeColorString(records)}}{{len(records)}}{{!hotCarsWeb.tweetLinks(records)}}
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /wsgi/views/old/data.tpl: -------------------------------------------------------------------------------- 1 |

Data Downloads

2 | 3 |

From this page you can download all of the data used to 4 | generate this website. All files are updated daily.

5 | 6 |

License

7 | 8 |

All DC Metro Metrics data is made available as open data 9 | under the 10 | 11 | Open Database License (ODbL) v1.0. 12 | 13 |

In summary, you are free to:

14 |

15 |

20 |

21 |

provided that:

22 |

23 |

28 |

29 | 30 |

Please see the license for full details.

31 | 32 |

HotCars Data

33 | 34 |

In accordance with the 35 | 36 | Twitter API terms of service, 37 | this dataset only include tweet ID's and Twitter user ID's.

38 | 39 |

HotCars Data (JSON)

40 |

HotCars Data (CSV)

41 | 42 |

Escalator Data

43 | 44 |

All escalator data was obtained in real-time through the WMATA API. 45 | 46 |

Escalator descriptions and statuses (JSON)

47 |

Escalator descriptions (CSV)

48 |

Escalator statuses (CSV)

49 | 50 | 51 | %rebase layout title='DC Metro Metrics: Data Downloads', description='' 52 | -------------------------------------------------------------------------------- /wsgi/views/old/escalator.tpl: -------------------------------------------------------------------------------- 1 | %# Listing for an escalator 2 | %from metroEscalatorsWeb import stationCodeToWebPath 3 | %from metroTimes import secondsToDHM, secondsToHMS, toLocalTime 4 | 5 | %description = "DC Metro Escalator {0} at {1} station, {2} {3}. Escalator performance history, and more." 6 | %description = description.format(unitId, escData['station_name'], escData['station_desc'], escData['esc_desc']) 7 | %#escalator data 8 |

Escalator {{unitId}}

9 | 10 | %stationWebPath = stationCodeToWebPath(escData['station_code']) 11 | 12 | 13 | 14 | 15 |
Station Name{{escData['station_name']}}
Station Code{{escData['station_code']}}
Station Description{{escData['station_desc']}}
Escalator Description{{escData['esc_desc']}}
16 | 17 | %#escalator summary 18 |

Summary

19 | 20 | 21 | 22 | 23 | 24 | 25 | %# 26 |
Num. Breaks{{escSummary['numBreaks']}}
Num. Fixes{{escSummary['numFixes']}}
Num. Inspections{{escSummary['numInspections']}}
Availability{{'%.3f%%'%(100.0*escSummary['availability'])}}
Report Time Range{{secondsToDHM(escSummary['absTime'])}}
Report Metro Open Time{{secondsToDHM(escSummary['metroOpenTime'])}}
27 | 28 |

History

29 | 30 | 31 | 32 | 33 | 34 | %# 35 | 36 | %for status in statuses: 37 | % tf = '%a, %m/%d/%y %I:%M %p' 38 | % localTime = toLocalTime(status['time']) 39 | % timeStr = localTime.strftime(tf) 40 | % durationStr = '' 41 | % if 'end_time' in status: 42 | % duration = status['end_time'] - status['time'] 43 | % durationStr = secondsToDHM(duration.total_seconds()) 44 | % end 45 | % tickDeltaStr = secondsToHMS(status.get('tickDelta',0.0)) 46 | 47 | 48 | 49 | 50 | %# 51 | 52 | %end 53 |
TimeStatusDurationTick Delta
{{timeStr}}{{status['symptom']}}{{durationStr}}{{tickDeltaStr}}
54 | 55 | %tf = '%m/%d/%y %I:%M %p' 56 | %updateStr = toLocalTime(curTime).strftime(tf) 57 |
58 |

Page Last Updated: {{updateStr}}

59 |
60 | 61 | %rebase layout title=unitId, description=description 62 | -------------------------------------------------------------------------------- /wsgi/views/old/escalatorOutages.tpl: -------------------------------------------------------------------------------- 1 | %# Page for a station 2 | %import metroEscalatorsWeb 3 | %from metroEscalatorsWeb import lineToColoredSquares, escUnitIdToWebPath, stationCodeToWebPath 4 | %from metroTimes import toLocalTime 5 | 6 | 7 |

Escalator Outages

8 | 9 |

Summary

10 | 11 | 12 | 13 |
Availability{{'%.2f%%'%(100.0*systemAvailability['availability'])}}
Weighted Availability{{'%.2f%%'%(100.0*systemAvailability['weightedAvailability'])}}
14 |

Symptom Counts

15 | 16 |
17 | 18 | %totalCount = sum(symptomCounts.itervalues()) 19 | %for i,(symptom,count) in enumerate(symptomCounts.most_common()): 20 | 21 | %end 22 | 23 |
{{symptom}}{{'%i'%(count)}}
TOTAL{{'%i'%totalCount}}
24 |
25 | 26 |
27 |
28 | 29 | 30 |

Outages

31 | 32 |
33 |
34 | 35 | %hasEntranceData = any(esc['stationDesc'] for esc in escList) 36 |
37 | 38 | 39 | %if hasEntranceData: 40 | 41 | 42 | 43 | 44 | %else: 45 | 46 | 47 | 48 | %end 49 | 50 | %for esc in escList: 51 | 52 | 53 | 54 | % if hasEntranceData: 55 | 56 | % end 57 | 58 | %# 59 | 60 | %end 61 |
UnitStationEntranceStatusUnitStationStatus
{{!esc['unitIdHtml']}}{{!esc['stationNameHtml']}}{{esc['stationDesc']}}{{esc['symptom']}}{{'%.2f%%'%(100.0*esc['availability'])}}
62 |
63 |
64 | 65 | %tf = '%m/%d/%y %I:%M %p' 66 | %updateStr = toLocalTime(curTime).strftime(tf) 67 |
68 |

Page Last Updated: {{updateStr}}

69 |
70 | 71 | 72 | 73 | 74 | %rebase layout title='DC Metro Metrics: Nonoperational Escalators', description='' 75 | -------------------------------------------------------------------------------- /wsgi/views/old/escalatorRankings.tpl: -------------------------------------------------------------------------------- 1 | %# Get rankings for escalators 2 | %from metroTimes import toLocalTime 3 | 4 | %description = 'Performance rankings of escalators in the WMATA Metrorail system.' 5 | 6 | 7 |

Rankings

8 | 9 |

Explore which escalators are the most broken, and which stations have the 10 | best and worst performing escalators. Also explore historical daily counts 11 | of escalator breaks and inspections.

12 | 13 |

14 | Escalator Rankings  15 | Station Rankings  16 | Trends  17 |

18 | 19 | 20 | 21 |

Escalator Rankings (top)

22 |

23 |

Glossary

24 | 25 | 26 |
27 | %# Disabling due to incorrect escaping of tags in text fields 28 | %#{{!dtRankings.ToHtml()}} 29 |
30 | 31 | 32 |
33 | 1d  34 | 3d  35 | 7d  36 | 14d  37 | 28d  38 | All Time

39 |
40 |
41 |
42 | 43 | 44 | 45 |

Station Rankings (top)

46 | Glossary 47 |
48 | 49 | 50 |

Trends (top)

51 |

Number of breaks (red) and inspections (blue) per day.

52 |
53 |

Note: "Data outages" refer to times when escalator data was not 54 | collected due to technical difficulties, resulting in low counts.

55 | 56 | 57 | %tf = '%m/%d/%y %I:%M %p' 58 | %updateStr = toLocalTime(curTime).strftime(tf) 59 |
60 |

Page Last Updated: {{updateStr}}

61 |
62 | 63 | 64 | 65 | 66 | %rebase layout title='DC Metro Metrics: Escalator Rankings', description=description 67 | -------------------------------------------------------------------------------- /wsgi/views/old/escalators.tpl: -------------------------------------------------------------------------------- 1 | %# Page for a station 2 | %import metroEscalatorsWeb 3 | %from metroEscalatorsWeb import lineToColoredSquares, escUnitIdToWebPath, stationCodeToWebPath 4 | %from metroTimes import toLocalTime 5 | 6 |
7 |
8 | 9 |

Escalators

10 | 11 | %stationNames = sorted(stationToEsc.keys()) 12 | %stationLetters = set(sn[0].upper() for sn in stationNames) 13 | %seenLetters = set() 14 | 15 | %#Create a list of bookmark links by letter 16 | % for letter in sorted(stationLetters): 17 | {{letter}} 18 | % end 19 | 20 | %for station in stationNames: 21 | % escList = stationToEsc[station] 22 | % if not escList: 23 | % continue 24 | % end 25 | % stationCode = escList[0]['stationCode'] 26 | % stationWebPath = stationCodeToWebPath(stationCode) 27 | % hasEntranceData = any(esc['stationDesc'] for esc in escList) 28 | % firstLetter = station[0].upper() 29 | % sawFirstLetter = firstLetter in seenLetters 30 | % if not sawFirstLetter: 31 | % seenLetters.add(firstLetter) 32 | % end 33 |
34 | % if sawFirstLetter: 35 |

{{station}}

36 | % else: 37 | % #Seeing this letter for first time, add a bookmark 38 | 39 |

{{station}}

40 | 41 | % end 42 | 43 | 44 | 45 | 46 | % if hasEntranceData: 47 | 48 | % end 49 | 50 | 51 | 52 | % for esc in escList: 53 | % escWebPath = escUnitIdToWebPath(esc['unitId']) 54 | 55 | 56 | % if hasEntranceData: 57 | 58 | % end 59 | 60 | 61 | %# 62 | 63 | % end 64 |
UnitEntranceDescriptionStatus
{{esc['unitId']}}{{esc['stationDesc']}}{{esc['escDesc']}}{{esc['symptom']}}{{'%.2f%%'%(100.0*esc['availability'])}}
65 |
66 | %end 67 | 68 | 69 | 70 | %tf = '%m/%d/%y %I:%M %p' 71 | %updateStr = toLocalTime(curTime).strftime(tf) 72 |
73 |

Page Last Updated: {{updateStr}}

74 |
75 | 76 |
77 |
78 | 79 | %rebase layout title='DC Metro Metrics: All Escalators' 80 | -------------------------------------------------------------------------------- /wsgi/views/old/footer.tpl: -------------------------------------------------------------------------------- 1 |

© 2013 by Lee M. Mendelowitz

2 | -------------------------------------------------------------------------------- /wsgi/views/old/header.tpl: -------------------------------------------------------------------------------- 1 | DC Metro Metrics 2 | -------------------------------------------------------------------------------- /wsgi/views/old/home.tpl: -------------------------------------------------------------------------------- 1 |

Welcome!

2 | 3 |

DC Metro Metrics is a project dedicated to collecting and sharing publicly available data related to the DC Metrorail system.

4 | 5 | This website compiles data which is not available anywhere else, including: 6 |

13 | 14 |

This website and its features are a work in progress. 15 | If you have any questions or comments, or would like to get involved, 16 | please contact me at info@dcmetrometrics.com.

17 | 18 |

Thanks for visiting!

19 | 20 |
21 | 22 | %rebase layout title='', description='' 23 | -------------------------------------------------------------------------------- /wsgi/views/old/hotCar.tpl: -------------------------------------------------------------------------------- 1 | %# Hot Car Page 2 | %from hotCarsWeb import formatTimeStr 3 | %from metroEscalatorsWeb import lineToColoredSquares 4 | %from metroTimes import toLocalTime 5 | 6 | %description = "Reports for #wmata #hotcar {0} of the WMATA Metrorail System.".format(carNum) 7 | 8 |

HotCar {{carNum}}

9 | 10 |

Summary

11 | 12 | %lineStr = lineToColoredSquares(colors) 13 | 14 | 15 | 16 | 17 |
Num. Reports{{numReports}}
Lines{{!lineStr}}
Last Report Time{{lastReportTimeStr}}
18 | 19 |

Reports

20 | 21 |
22 | %for report in reports: 23 |
24 | {{!formatTimeStr(report['time'])}} 25 | {{!report['embed_html']}} 26 |
27 |
28 | %end 29 |
30 | 31 | %tf = '%m/%d/%y %I:%M %p' 32 | %updateStr = toLocalTime(curTime).strftime(tf) 33 |
34 |

Page Last Updated: {{updateStr}}

35 |
36 | 37 | %rebase layout title='Hot Car %i'%carNum, description=description 38 | -------------------------------------------------------------------------------- /wsgi/views/old/layout.tpl: -------------------------------------------------------------------------------- 1 | %# BASIC LAYOUT 2 | 3 | 4 | %description_default = "Sharing public and crowdsourced data related to the Washington DC Metrorail system, including data on escalator outages, escalator historical performance, and #wmata #hotcar's." 5 | 6 | 7 | 8 | 9 | 10 | {{title or 'DC Metro Metrics'}} 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 34 | 35 |
36 | %include left_column 37 |
38 | 39 |
40 | %include 41 |
42 | 43 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /wsgi/views/old/left_column.tpl: -------------------------------------------------------------------------------- 1 | %from metroEscalatorsWeb import PATHS 2 | 29 | -------------------------------------------------------------------------------- /wsgi/views/old/station.tpl: -------------------------------------------------------------------------------- 1 | %# Page for a station 2 | %import metroEscalatorsWeb 3 | %from metroEscalatorsWeb import lineToColoredSquares, escUnitIdToWebPath 4 | %from metroTimes import toLocalTime 5 | 6 | %#station data 7 | %codeStr = ', '.join(stationSnapshot['allCodes']) 8 | %lineStr = lineToColoredSquares(stationSnapshot['lineCodes']) 9 | %numRiderStr = '{:,d}'.format(stationSnapshot['numRiders']) 10 | 11 |

{{stationSnapshot['name']}}

12 | 13 |

Station Data

14 | 15 | 16 | 17 | 18 | 19 |
Station Name{{stationSnapshot['name']}}
Station Codes{{codeStr}}
Station Lines{{!lineStr}}
Avg. Weekday Ridership (Oct 2012){{numRiderStr}}
20 | 21 |

Escalator Summary

22 | 23 | 24 | 25 | 26 | 27 |
# Escalators{{stationSnapshot['numEscalators']}}
# Working Escalators{{stationSnapshot['numWorking']}}
Current Availability{{'%.2f%%'%(100.0*stationSnapshot['availability'])}}
Avg. Availability{{'%.2f%%'%(100.0*stationSummary['availability'])}}
28 | 29 | 30 |

Escalators

31 | %hasStationDesc = any(e['stationDesc'] for e in escalators) 32 | 33 | 34 | 35 | %if hasStationDesc: 36 | 37 | %end 38 | 39 | 40 | 41 | 42 | %for esc in escalators: 43 | % escWebPath = escUnitIdToWebPath(esc['unitId']) 44 | 45 | 46 | %if hasStationDesc: 47 | 48 | %end 49 | 50 | 51 | 52 | 53 | %end 54 |
UnitEntranceDescriptionCurrent StatusAvg. Availabilty
{{esc['unitId']}}{{esc['stationDesc']}}{{esc['escDesc']}}{{esc['curStatus']}}{{'%.2f%%'%(100.0*esc['availability'])}}
55 | 56 | %tf = '%m/%d/%y %I:%M %p' 57 | %updateStr = toLocalTime(curTime).strftime(tf) 58 |
59 |

Page Last Updated: {{updateStr}}

60 |
61 | 62 | %rebase layout title=stationSnapshot['name'], description='' 63 | -------------------------------------------------------------------------------- /wsgi/views/old/stations.tpl: -------------------------------------------------------------------------------- 1 | %# Listing of all stations 2 | %from operator import itemgetter 3 | %import metroEscalatorsWeb 4 | %from metroTimes import toLocalTime 5 | 6 | 7 |

Stations

8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | %# 18 | 19 | 20 | %stationRecs = sorted(stationRecs, key = itemgetter('name')) 21 | %for i,rec in enumerate(stationRecs): 22 | % rowClass = 'oddRow' if i%2 == 1 else 'evenRow' 23 | % codeStr = ', '.join(rec['codes']) 24 | % lineStr = ', '.join(rec['lines']) 25 | % lineStr = metroEscalatorsWeb.lineToColoredSquares(rec['lines']) 26 | % stationWebPath = metroEscalatorsWeb.stationCodeToWebPath(rec['codes'][0]) 27 | 28 | 29 | 30 | 31 | 32 | %# 33 | 34 | 35 | %end 36 |
Station NameLinesStation CodeEscalatorsAvailable EscalatorsAvailability
{{!rec['name']}}{{!lineStr}}{{codeStr}}{{str(rec['numEscalators'])}}{{rec['numWorking']}}{{'%.1f%%'%(100.0*rec['availability'])}}
37 |
38 | 39 | %tf = '%m/%d/%y %I:%M %p' 40 | %updateStr = toLocalTime(curTime).strftime(tf) 41 |
42 |

Page Last Updated: {{updateStr}}

43 |
44 | 45 | 46 | 47 | 48 | %rebase layout title='DC Metro Metrics: Stations', description='' 49 | -------------------------------------------------------------------------------- /wsgi/views/old/stations_js.tpl: -------------------------------------------------------------------------------- 1 | %#Generate javascript for the symptom table 2 | google.load('visualization', '1', {packages: ['corechart', 'table']}); 3 | 4 | var stationsJson = {{!dtStations.ToJSon()}}; 5 | 6 | // Set the property for an entire datatable row, cell by cell 7 | var setRowProperty = function(dt, rowNum, property) 8 | { 9 | var numCols = dt.getNumberOfColumns(); 10 | for(var i=0; i 8 |
9 | 10 |

Stations

11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | %# 21 | 22 | 23 | %stationRecs = sorted(stationRecs, key = itemgetter('name')) 24 | %for i,rec in enumerate(stationRecs): 25 | % rowClass = 'oddRow' if i%2 == 1 else 'evenRow' 26 | % codeStr = ', '.join(rec['codes']) 27 | % lineStr = ', '.join(rec['lines']) 28 | % lineStr = metroEscalatorsWeb.lineToColoredSquares(rec['lines']) 29 | % stationWebPath = metroEscalatorsWeb.stationCodeToWebPath(rec['codes'][0]) 30 | 31 | 32 | 33 | 34 | 35 | %# 36 | 37 | 38 | %end 39 |
Station NameLinesStation CodeEscalatorsAvailable EscalatorsAvailability
{{!rec['name']}}{{!lineStr}}{{codeStr}}{{str(rec['numEscalators'])}}{{rec['numWorking']}}{{'%.1f%%'%(100.0*rec['availability'])}}
40 |
41 | 42 | %tf = '%m/%d/%y %I:%M %p' 43 | %updateStr = toLocalTime(curTime).strftime(tf) 44 |
45 |

Page Last Updated: {{updateStr}}

46 |
47 | 48 |
49 | 50 | 51 | %def scriptsToInclude(): 52 | 53 | 54 | %end 55 | 56 | %rebase layout title='DC Metro Metrics: Stations', scriptsToInclude=scriptsToInclude 57 | -------------------------------------------------------------------------------- /wsgi/views/stations_js.tpl: -------------------------------------------------------------------------------- 1 | %#Generate javascript for the symptom table 2 | google.load('visualization', '1', {packages: ['corechart', 'table']}); 3 | 4 | var stationsJson = {{!dtStations.ToJSon()}}; 5 | 6 | // Set the property for an entire datatable row, cell by cell 7 | var setRowProperty = function(dt, rowNum, property) 8 | { 9 | var numCols = dt.getNumberOfColumns(); 10 | for(var i=0; i