├── log └── .keep ├── storage └── .keep ├── tmp ├── .keep └── pids │ └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep └── nyct-subway.pb.rb ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── .well-known │ ├── assetlinks.json │ └── apple-app-site-association ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ └── .keep ├── system │ └── .keep ├── controllers │ └── .keep ├── integration │ └── .keep ├── fixtures │ └── files │ │ └── .keep ├── application_system_test_case.rb ├── channels │ └── application_cable │ │ └── connection_test.rb └── test_helper.rb ├── .ruby-version ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── slack.gif │ │ ├── favicon.png │ │ ├── screenshot.png │ │ ├── slack-help.gif │ │ ├── apple-touch-icon.png │ │ └── cross-15.svg │ ├── config │ │ ├── manifest.js │ │ └── manifest.webmanifest │ └── stylesheets │ │ └── application.css ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── scheduled │ │ ├── route.rb │ │ ├── connection.rb │ │ ├── bus_transfer.rb │ │ ├── transfer.rb │ │ ├── calendar_exception.rb │ │ ├── schedule.rb │ │ ├── stop_time.rb │ │ ├── stop.rb │ │ └── trip.rb │ ├── service_changes │ │ ├── no_train_service_change.rb │ │ ├── not_scheduled_service_change.rb │ │ ├── truncated_service_change.rb │ │ ├── rerouting_service_change.rb │ │ ├── express_to_local_service_change.rb │ │ ├── local_to_express_service_change.rb │ │ ├── split_routing_service_change.rb │ │ └── service_change.rb │ ├── long_term_service_change_regular_routing.rb │ ├── delay_notification.rb │ └── trip.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── application_controller.rb │ ├── index_controller.rb │ ├── slack_controller.rb │ ├── api │ │ ├── stats_controller.rb │ │ ├── trips_controller.rb │ │ └── routes_controller.rb │ └── oauth_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── index │ │ └── index.html.erb │ └── slack │ │ ├── index.html.erb │ │ ├── help.html.erb │ │ └── privacy.html.erb ├── javascript │ ├── packs │ │ ├── components │ │ │ ├── twitterModal.scss │ │ │ ├── history.js │ │ │ ├── images │ │ │ │ └── googleplay.png │ │ │ ├── trainMap.scss │ │ │ ├── footer.scss │ │ │ ├── accessibility.scss │ │ │ ├── trainGrid.scss │ │ │ ├── stationList.scss │ │ │ ├── train.scss │ │ │ ├── trainModalOverviewPane.scss │ │ │ ├── trainModal.scss │ │ │ ├── trainBullet.scss │ │ │ ├── tripModal.scss │ │ │ ├── accessibility.jsx │ │ │ ├── train.jsx │ │ │ ├── trainBullet.jsx │ │ │ ├── utils.jsx │ │ │ ├── stationModal.scss │ │ │ ├── trainMapStop.scss │ │ │ ├── trainGrid.jsx │ │ │ ├── footer.jsx │ │ │ ├── trainModalOverviewPane.jsx │ │ │ ├── aboutModal.jsx │ │ │ ├── trainModalDirectionPane.scss │ │ │ └── app.scss │ │ ├── application.js │ │ └── vendor │ │ │ └── smartbanner.min.css │ └── channels │ │ ├── index.js │ │ └── consumer.js ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── helpers │ ├── application_helper.rb │ └── twitter_helper.rb ├── mailers │ └── application_mailer.rb ├── workers │ ├── routing_refresh_worker.rb │ ├── redis_cleanup_worker.rb │ ├── feed_retriever_spawning_a_worker.rb │ ├── feed_retriever_spawning_b1_worker.rb │ ├── feed_retriever_spawning_b2_worker.rb │ ├── feed_retriever_spawning_worker.rb │ ├── route_processor_worker.rb │ ├── feed_retriever_spawning_worker_base.rb │ ├── scheduled_times_refresh_worker.rb │ ├── travel_times_refresh_worker.rb │ ├── accessibility_list_worker.rb │ ├── accessibility_statuses_worker.rb │ ├── feed_retriever_worker.rb │ └── heroku_autoscaler_worker.rb └── jobs │ └── application_job.rb ├── .browserslistrc ├── Procfile ├── config ├── spring.rb ├── environment.rb ├── webpack │ ├── test.js │ ├── production.js │ ├── development.js │ └── environment.js ├── initializers │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── redis.rb │ ├── permissions_policy.rb │ ├── wrap_parameters.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── inflections.rb │ ├── content_security_policy.rb │ ├── sidekiq.rb │ └── new_framework_defaults_6_1.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── locales │ └── en.yml ├── application.rb ├── storage.yml ├── routes.rb ├── puma.rb ├── webpacker.yml └── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── bin ├── rake ├── rails ├── webpack ├── webpack-dev-server ├── spring ├── yarn ├── setup └── bundle ├── config.ru ├── db └── migrate │ ├── 20210314204626_drop_parent_stop_id_from_stops.rb │ ├── 20210314204824_add_secondary_name_to_stops.rb │ ├── 20240613040357_fix_sir_color.rb │ ├── 20240217002930_remove_broadway_jct_lirr_connection.rb │ ├── 20220102081218_add_latitude_and_longitude_to_stops.rb │ ├── 20210329005640_add_more_indexes.rb │ ├── 20211002063906_add_access_time_for_transfers.rb │ ├── 20210123052305_drop_directional_stops.rb │ ├── 20240102074539_update_fs_alternate_name.rb │ ├── 20211003215534_seed_missing_si_ferry_connection.rb │ ├── 20211003182053_seed_connection_for_gwb_bus_station.rb │ ├── 20210123172430_drop_n12_stop.rb │ ├── 20210122182736_remove_h19_station.rb │ ├── 20211015052833_seed_more_bus_transfers_and_connections.rb │ ├── 20211003014720_add_connections.rb │ ├── 20210123050349_migrate_stop_times.rb │ ├── 20220102081654_seed_stops_geolocations.rb │ ├── 20211015051225_add_unique_indexes_and_foreign_keys_to_bus_transfers_and_connections.rb │ ├── 20211002202553_add_bus_transfers.rb │ ├── 20240330050715_on_update_cascade_schedule_f_ks.rb │ ├── 20230309061906_seed_gcm_connections.rb │ ├── 20211002064045_seed_times_sq_to_bryant_pk_transfer.rb │ ├── 20240520022419_use_official_mta_colors.rb │ ├── 20201229201218_create_delayed_jobs.rb │ └── 20210102223417_initialize_scheduled_models.rb ├── import └── README.md ├── Rakefile ├── postcss.config.js ├── .gitattributes ├── package.json ├── .gitignore ├── LICENSE ├── babel.config.js └── Gemfile /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.3 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/index/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component('App', {}, { class: 'app' }) %> -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rails s 2 | worker: bundle exec sidekiq -q critical -q default -q low -c 5 -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/javascript/packs/components/twitterModal.scss: -------------------------------------------------------------------------------- 1 | .twitter-modal .small.route.bullet { 2 | margin-left: 0; 3 | } -------------------------------------------------------------------------------- /app/assets/images/slack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahblahblah-/subwaynow-server/HEAD/app/assets/images/slack.gif -------------------------------------------------------------------------------- /app/controllers/index_controller.rb: -------------------------------------------------------------------------------- 1 | class IndexController < ApplicationController 2 | def index 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahblahblah-/subwaynow-server/HEAD/app/assets/images/favicon.png -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link ./manifest.webmanifest -------------------------------------------------------------------------------- /app/assets/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahblahblah-/subwaynow-server/HEAD/app/assets/images/screenshot.png -------------------------------------------------------------------------------- /app/assets/images/slack-help.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahblahblah-/subwaynow-server/HEAD/app/assets/images/slack-help.gif -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/javascript/packs/components/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export default createBrowserHistory(); -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /app/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahblahblah-/subwaynow-server/HEAD/app/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def title(page_title) 3 | content_for(:title) { page_title } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | require_relative "../config/boot" 4 | require "rake" 5 | Rake.application.run 6 | -------------------------------------------------------------------------------- /app/javascript/packs/components/images/googleplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahblahblah-/subwaynow-server/HEAD/app/javascript/packs/components/images/googleplay.png -------------------------------------------------------------------------------- /app/models/scheduled/route.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::Route < ActiveRecord::Base 2 | default_scope { order(name: :asc) } 3 | scope :visible, -> { where(visible: true)} 4 | end -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/controllers/slack_controller.rb: -------------------------------------------------------------------------------- 1 | class SlackController < ApplicationController 2 | def index 3 | end 4 | 5 | def help 6 | end 7 | 8 | def privacy 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /app/models/service_changes/no_train_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::NoTrainServiceChange < ServiceChanges::ServiceChange 2 | def applicable_to_routing?(routing) 3 | true 4 | end 5 | end -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /app/models/scheduled/connection.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::Connection < ActiveRecord::Base 2 | belongs_to :from_stop, class_name: "Stop", foreign_key: "from_stop_internal_id", primary_key: "internal_id" 3 | end -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /db/migrate/20210314204626_drop_parent_stop_id_from_stops.rb: -------------------------------------------------------------------------------- 1 | class DropParentStopIdFromStops < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_column :stops, :parent_stop_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210314204824_add_secondary_name_to_stops.rb: -------------------------------------------------------------------------------- 1 | class AddSecondaryNameToStops < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :stops, :secondary_name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/javascript/packs/components/trainMap.scss: -------------------------------------------------------------------------------- 1 | .train-map { 2 | ul { 3 | list-style-type: none; 4 | text-align: left; 5 | margin: auto; 6 | padding: 0; 7 | margin-bottom: .5em; 8 | } 9 | } -------------------------------------------------------------------------------- /app/models/service_changes/not_scheduled_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::NotScheduledServiceChange < ServiceChanges::ServiceChange 2 | def applicable_to_routing?(routing) 3 | true 4 | end 5 | end -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | APP_PATH = File.expand_path('../config/application', __dir__) 4 | require_relative "../config/boot" 5 | require "rails/commands" 6 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | environment.loaders.get('sass').use.splice(-1, 0, { 4 | loader: 'resolve-url-loader', 5 | }); 6 | 7 | module.exports = environment 8 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /db/migrate/20240613040357_fix_sir_color.rb: -------------------------------------------------------------------------------- 1 | class FixSirColor < ActiveRecord::Migration[7.1] 2 | def change 3 | route = Scheduled::Route.find_by!(internal_id: "SI") 4 | route.color = "0039a6" 5 | route.save! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /import/README.md: -------------------------------------------------------------------------------- 1 | # Import 2 | 3 | Please put the most up-to-date MTA static schedule data from http://web.mta.info/developers/developer-data-terms.html in this directory unzipped, as well as the the Stations Locations (Stations.csv) file. 4 | -------------------------------------------------------------------------------- /app/javascript/packs/components/footer.scss: -------------------------------------------------------------------------------- 1 | .footer.ui.vertical.segment { 2 | padding: 1em 2em; 3 | 4 | .ui.inverted.list .item a:not(.ui):hover, .footer .ui.inverted.list .item a:not(.ui):active { 5 | color: #ffe21f !important; 6 | } 7 | } -------------------------------------------------------------------------------- /app/workers/routing_refresh_worker.rb: -------------------------------------------------------------------------------- 1 | class RoutingRefreshWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'default' 4 | 5 | def perform 6 | ServiceChangeAnalyzer.refresh_routings(Time.current.to_i) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: goodservice_v2_production 11 | -------------------------------------------------------------------------------- /db/migrate/20240217002930_remove_broadway_jct_lirr_connection.rb: -------------------------------------------------------------------------------- 1 | class RemoveBroadwayJctLirrConnection < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Connection.find_by(from_stop_internal_id: "A51", name: "LIRR")&.destroy 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | 4 | const channels = require.context('.', true, /_channel\.js$/) 5 | channels.keys().forEach(channels) 6 | -------------------------------------------------------------------------------- /db/migrate/20220102081218_add_latitude_and_longitude_to_stops.rb: -------------------------------------------------------------------------------- 1 | class AddLatitudeAndLongitudeToStops < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :stops, :latitude, :decimal 4 | add_column :stops, :longitude, :decimal 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/scheduled/bus_transfer.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::BusTransfer < ActiveRecord::Base 2 | belongs_to :from_stop, class_name: "Stop", foreign_key: "from_stop_internal_id", primary_key: "internal_id" 3 | 4 | def is_sbs? 5 | bus_route.include?("SBS") 6 | end 7 | end -------------------------------------------------------------------------------- /db/migrate/20210329005640_add_more_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddMoreIndexes < ActiveRecord::Migration[6.1] 2 | def change 3 | add_index :trips, :schedule_service_id 4 | add_index :trips, :route_internal_id 5 | add_index :stop_times, :departure_time 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20211002063906_add_access_time_for_transfers.rb: -------------------------------------------------------------------------------- 1 | class AddAccessTimeForTransfers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :transfers, :access_time_from, :integer 4 | add_column :transfers, :access_time_to, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20210123052305_drop_directional_stops.rb: -------------------------------------------------------------------------------- 1 | class DropDirectionalStops < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Stop.where("internal_id like ?", "%N").destroy_all 4 | Scheduled::Stop.where("internal_id like ?", "%S").destroy_all 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20240102074539_update_fs_alternate_name.rb: -------------------------------------------------------------------------------- 1 | class UpdateFsAlternateName < ActiveRecord::Migration[6.1] 2 | def change 3 | route = Scheduled::Route.find_by!(internal_id: 'FS') 4 | route.alternate_name = 'Franklin Av Shuttle' 5 | route.save! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/workers/redis_cleanup_worker.rb: -------------------------------------------------------------------------------- 1 | class RedisCleanupWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'low' 4 | 5 | def perform 6 | RedisStore.clear_outdated_trips 7 | RedisStore.clear_outdated_trip_stops_and_delays 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20211003215534_seed_missing_si_ferry_connection.rb: -------------------------------------------------------------------------------- 1 | class SeedMissingSiFerryConnection < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Connection.create!(from_stop_internal_id: "S31", name: "SI Ferry", mode: "ship", min_transfer_time: 300) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /db/migrate/20211003182053_seed_connection_for_gwb_bus_station.rb: -------------------------------------------------------------------------------- 1 | class SeedConnectionForGwbBusStation < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Connection.create!(from_stop_internal_id: "A07", name: "GWB Bus Station", mode: "bus", min_transfer_time: 300) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/models/scheduled/transfer.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::Transfer < ActiveRecord::Base 2 | belongs_to :from_stop, class_name: "Stop", foreign_key: "from_stop_internal_id", primary_key: "internal_id" 3 | belongs_to :to_stop, class_name: "Stop", foreign_key: "to_stop_internal_id", primary_key: "internal_id" 4 | end -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/models/service_changes/truncated_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::TruncatedServiceChange < ServiceChanges::ServiceChange 2 | def applicable_to_routing?(routing) 3 | if begin_of_route? 4 | destinations.include?(routing.last) 5 | else 6 | routing.last == first_station 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. 3 | 4 | import { createConsumer } from "@rails/actioncable" 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /app/javascript/packs/components/accessibility.scss: -------------------------------------------------------------------------------- 1 | .accessible-icon { 2 | i.icons { 3 | .blue.accessible.icon { 4 | margin-right: .25em !important; 5 | } 6 | 7 | .corner.icon { 8 | text-shadow: none; 9 | } 10 | } 11 | 12 | &.not-accessible { 13 | margin-left: .5em; 14 | } 15 | } -------------------------------------------------------------------------------- /app/models/service_changes/rerouting_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::ReroutingServiceChange < ServiceChanges::ServiceChange 2 | def applicable_to_routing?(routing) 3 | if begin_of_route? 4 | destinations.include?(routing.last) 5 | else 6 | routing.include?(first_station) 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Subway Now lite", 3 | "short_name": "Subway Now lite", 4 | "start_url": "https://lite.subwaynow.app", 5 | "display": "fullscreen", 6 | "background_color": "#000000", 7 | "description": "New York City Subway Status Page", 8 | "scope": "https://lite.subwaynow.app/" 9 | } -------------------------------------------------------------------------------- /app/models/service_changes/express_to_local_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::ExpressToLocalServiceChange < ServiceChanges::ServiceChange 2 | def convert_to_rerouting 3 | ServiceChanges::ReroutingServiceChange.new( 4 | self.direction, self.stations_affected, self.origin, self.routing, self.long_term_override 5 | ) 6 | end 7 | end -------------------------------------------------------------------------------- /test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase 4 | # test "connects with cookies" do 5 | # cookies.signed[:user_id] = 42 6 | # 7 | # connect 8 | # 9 | # assert_equal connection.user_id, "42" 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/stats_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::StatsController < ApplicationController 2 | def index 3 | data = { 4 | feeds: RedisStore.all_feed_latencies.transform_values(&:to_f), 5 | routes: RedisStore.all_processed_route_latencies.transform_values(&:to_f), 6 | } 7 | render json: data 8 | end 9 | end -------------------------------------------------------------------------------- /db/migrate/20210123172430_drop_n12_stop.rb: -------------------------------------------------------------------------------- 1 | class DropN12Stop < ActiveRecord::Migration[6.1] 2 | def change 3 | # N12 is an internal station for S.B. Coney Island 4 | Scheduled::Stop.find_by(internal_id: 'N12')&.destroy 5 | Scheduled::Stop.find_by(internal_id: 'N12N')&.destroy 6 | Scheduled::Stop.find_by(internal_id: 'N12S')&.destroy 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20210122182736_remove_h19_station.rb: -------------------------------------------------------------------------------- 1 | class RemoveH19Station < ActiveRecord::Migration[6.1] 2 | def change 3 | # H19 is an internal station for Broad Channel 4 | Scheduled::Stop.find_by(internal_id: 'H19')&.destroy 5 | Scheduled::Stop.find_by(internal_id: 'H19N')&.destroy 6 | Scheduled::Stop.find_by(internal_id: 'H19S')&.destroy 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark the yarn lockfile as having been generated. 7 | yarn.lock linguist-generated 8 | 9 | # Mark any vendored files as having been vendored. 10 | vendor/* linguist-vendored 11 | -------------------------------------------------------------------------------- /app/models/service_changes/local_to_express_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::LocalToExpressServiceChange < ServiceChanges::ServiceChange 2 | CLOSED_STOPS = ENV['CLOSED_STOPS']&.split(',')&.map {|s| s[0..2]} || [] 3 | 4 | def not_long_term? 5 | !long_term? 6 | end 7 | 8 | def long_term? 9 | (intermediate_stations - CLOSED_STOPS).empty? 10 | end 11 | end -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'uri' 3 | 4 | if Rails.env == 'production' 5 | if ENV['REDIS_URL'] 6 | REDIS_CLIENT = Redis.new(url: ENV["REDIS_URL"], ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }) 7 | else 8 | REDIS_CLIENT = Redis.new(url: ENV['REDISCLOUD_URL']) 9 | end 10 | else 11 | REDIS_CLIENT = Redis.new(url: ENV['REDIS_URL']) 12 | end 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/.well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "relation": ["delegate_permission/common.handle_all_urls"], 4 | "target": { 5 | "namespace": "android_app", 6 | "package_name": "io.goodservice.theweekendest", 7 | "sha256_cert_fingerprints": 8 | ["90:8E:AB:7B:BD:65:D0:34:2B:06:A9:52:08:FC:86:07:7F:6E:BE:00:DE:FE:51:E9:44:37:4E:D3:16:70:DB:72"] 9 | } 10 | } 11 | ] -------------------------------------------------------------------------------- /db/migrate/20211015052833_seed_more_bus_transfers_and_connections.rb: -------------------------------------------------------------------------------- 1 | class SeedMoreBusTransfersAndConnections < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::BusTransfer.create!(from_stop_internal_id: "J12", bus_route: "Q10 to JFK", min_transfer_time: 300, airport_connection: true) 4 | Scheduled::Connection.create!(from_stop_internal_id: "418", name: "PATH", mode: "subway", min_transfer_time: 300) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | class ActiveSupport::TestCase 6 | # Run tests in parallel with specified workers 7 | parallelize(workers: :number_of_processors) 8 | 9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /app/workers/feed_retriever_spawning_a_worker.rb: -------------------------------------------------------------------------------- 1 | class FeedRetrieverSpawningAWorker < FeedRetrieverSpawningWorkerBase 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'critical' 4 | 5 | FEEDS = [""] 6 | 7 | def perform 8 | minutes = Time.current.min 9 | fraction_of_minute = Time.current.sec / 5 10 | FEEDS.each do |id| 11 | FeedRetrieverWorker.perform_async(id, minutes, fraction_of_minute) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | w6zKdzfXW94pP2aGOaMRIa/++sJu8EvO6gyMBN674agpF1zg/nq0FWfO8/sHd3VWMh1x+ttZhVUze9j8dKyYwenidpUmy55gwkwTjLdSJ0+RhqcpkK5KYdz/2QF6CT/CJQ/jXnRYEqYMhR961ZE0V93axTpjd5Ec6/zNMRHTO+dzX7G1f9jUFXp0jBeLo3ZPbMhJ6UIZg/aO213/RSlzX9B/LcR4DHzSB7Soim1J9Fxiz/ezFJ/01s7jrV8R63jvrhdM9fKNNo+GWbTIRlrMxKyJYu7pN8hWSEtWy6Z87+mOIvhuJRM9lYkitPWUvP9uaH3lNGiso9HcDRLG7JPvQxIvylFtfLv5wwM7nb0V/f1mhBDkedSlgJvkhrlB28Y7c1W5idk/vvBJgLvNxAI4I6lHGs0vEGtOgC92--omEhN3W83HQhnwkh--UlTSpJ8ILMzc0RWZv7pEyg== -------------------------------------------------------------------------------- /app/workers/feed_retriever_spawning_b1_worker.rb: -------------------------------------------------------------------------------- 1 | class FeedRetrieverSpawningB1Worker < FeedRetrieverSpawningWorkerBase 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'critical' 4 | 5 | FEEDS = ["-ace", "-bdfm", "-g"] 6 | 7 | def perform 8 | minutes = Time.current.min 9 | fraction_of_minute = Time.current.sec / 15 10 | FEEDS.each do |id| 11 | FeedRetrieverWorker.perform_async(id, minutes, fraction_of_minute) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/workers/feed_retriever_spawning_b2_worker.rb: -------------------------------------------------------------------------------- 1 | class FeedRetrieverSpawningB2Worker < FeedRetrieverSpawningWorkerBase 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'critical' 4 | 5 | FEEDS = ["-jz", "-nqrw", "-l", "-si"] 6 | 7 | def perform 8 | minutes = Time.current.min 9 | fraction_of_minute = Time.current.sec / 15 10 | FEEDS.each do |id| 11 | FeedRetrieverWorker.perform_async(id, minutes, fraction_of_minute) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/packs/components/trainGrid.scss: -------------------------------------------------------------------------------- 1 | .ui.grid.train-grid { 2 | @media only screen and (max-width: 767px) { 3 | display: none; 4 | } 5 | } 6 | 7 | .ui.grid+.grid.mobile-train-grid { 8 | @media only screen and (min-width: 768px) { 9 | display: none; 10 | } 11 | 12 | margin-top: 0; 13 | 14 | .status { 15 | font-weight: 200; 16 | } 17 | 18 | .train-status-row { 19 | padding-bottom: 0; 20 | } 21 | 22 | .row:first-child { 23 | padding-top: 0; 24 | } 25 | } -------------------------------------------------------------------------------- /app/models/scheduled/calendar_exception.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::CalendarException < ActiveRecord::Base 2 | belongs_to :schedule, foreign_key: "schedule_service_id", primary_key: "service_id" 3 | 4 | def self.next_weekday 5 | date = Date.current 6 | while [0, 6].include?(date.wday) || 7 | where(date: date).where("schedule_service_id like ?", "%Sunday%").or(where(date: date).where("schedule_service_id like ?", "%Saturday%")).present? 8 | date += 1 9 | end 10 | date 11 | end 12 | end -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /app/workers/feed_retriever_spawning_worker.rb: -------------------------------------------------------------------------------- 1 | class FeedRetrieverSpawningWorker < FeedRetrieverSpawningWorkerBase 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'critical' 4 | 5 | FEEDS = ["", "-bdfm", "-ace", "-jz", "-nqrw", "-l", "-si", "-g"] 6 | 7 | def perform 8 | minutes = Time.current.min 9 | fraction_of_minute = Time.current.sec / 15 10 | FEEDS.each do |id| 11 | FeedRetrieverWorker.perform_async(id, minutes, fraction_of_minute) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/workers/route_processor_worker.rb: -------------------------------------------------------------------------------- 1 | class RouteProcessorWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'critical' 4 | 5 | def perform(route_id, timestamp) 6 | marshaled_trips = RedisStore.route_trips(route_id, timestamp) 7 | trips = Marshal.load(marshaled_trips) if marshaled_trips 8 | 9 | if !trips 10 | raise "Error: Trips for #{route_id} at #{Time.zone.at(timestamp)} not found" 11 | end 12 | 13 | RouteProcessor.process_route(route_id, trips, timestamp) 14 | end 15 | end -------------------------------------------------------------------------------- /db/migrate/20211003014720_add_connections.rb: -------------------------------------------------------------------------------- 1 | class AddConnections < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :connections, force: :cascade do |t| 4 | t.string :from_stop_internal_id, null: false 5 | t.string :name, null: false 6 | t.string :mode 7 | t.integer :min_transfer_time, default: 0, null: false 8 | t.integer :access_time_from 9 | t.integer :access_time_to 10 | t.index ["from_stop_internal_id"], name: "index_connections_on_from_stop_internal_id" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) 3 | gem "bundler" 4 | require "bundler" 5 | 6 | # Load Spring without loading other gems in the Gemfile, for speed. 7 | Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| 8 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 9 | gem "spring", spring.version 10 | require "spring/binstub" 11 | rescue Gem::LoadError 12 | # Ignore when Spring is not installed. 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20210123050349_migrate_stop_times.rb: -------------------------------------------------------------------------------- 1 | class MigrateStopTimes < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Stop.where("internal_id like ?", "%N").each do |stop| 4 | Scheduled::StopTime.where("stop_internal_id = ?", stop.internal_id).update_all(stop_internal_id: stop.internal_id[0..2]) 5 | end 6 | Scheduled::Stop.where("internal_id like ?", "%S").each do |stop| 7 | Scheduled::StopTime.where("stop_internal_id = ?", stop.internal_id).update_all(stop_internal_id: stop.internal_id[0..2]) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /db/migrate/20220102081654_seed_stops_geolocations.rb: -------------------------------------------------------------------------------- 1 | class SeedStopsGeolocations < ActiveRecord::Migration[6.1] 2 | def change 3 | csv_text = File.read(Rails.root.join('import', 'Stations.csv')) 4 | csv = CSV.parse(csv_text, headers: true) 5 | csv.each do |row| 6 | stop_id = row['GTFS Stop ID'] 7 | stop = Scheduled::Stop.find_by!(internal_id: stop_id) 8 | stop.latitude = row['GTFS Latitude'] 9 | stop.longitude = row['GTFS Longitude'] 10 | stop.save! 11 | puts "Geolocation for #{stop_id} saved" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/workers/feed_retriever_spawning_worker_base.rb: -------------------------------------------------------------------------------- 1 | class FeedRetrieverSpawningWorkerBase 2 | ALL_FEEDS = ["", "-ace", "-bdfm", "-g", "-jz", "-nqrw", "-l", "-si"] 3 | MISC_FEED_MAPPING = { 4 | "FS" => "-bdfm", 5 | "GS" => "", 6 | "H" => "-ace", 7 | "SI" => "-si", 8 | "SS" => "-si", 9 | } 10 | 11 | def self.feed_id_for(route_id) 12 | if MISC_FEED_MAPPING[route_id] 13 | MISC_FEED_MAPPING[route_id] 14 | elsif feed_id = ALL_FEEDS.find { |f| f.upcase.include?(route_id.first) } 15 | feed_id 16 | else 17 | "" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /db/migrate/20211015051225_add_unique_indexes_and_foreign_keys_to_bus_transfers_and_connections.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexesAndForeignKeysToBusTransfersAndConnections < ActiveRecord::Migration[6.1] 2 | def change 3 | add_index :bus_transfers, [:from_stop_internal_id, :bus_route], unique: true 4 | add_index :connections, [:from_stop_internal_id, :name], unique: true 5 | 6 | add_foreign_key :bus_transfers, :stops, column: :from_stop_internal_id, primary_key: :internal_id 7 | add_foreign_key :connections, :stops, column: :from_stop_internal_id, primary_key: :internal_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec yarn, *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20211002202553_add_bus_transfers.rb: -------------------------------------------------------------------------------- 1 | class AddBusTransfers < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :bus_transfers, force: :cascade do |t| 4 | t.string :from_stop_internal_id, null: false 5 | t.string :bus_route, null: false 6 | t.integer :min_transfer_time, default: 0, null: false 7 | t.integer :access_time_from 8 | t.integer :access_time_to 9 | t.boolean :airport_connection, default: false, null: false 10 | t.index ["from_stop_internal_id"], name: "index_bus_transfers_on_from_stop_internal_id" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20240330050715_on_update_cascade_schedule_f_ks.rb: -------------------------------------------------------------------------------- 1 | class OnUpdateCascadeScheduleFKs < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_foreign_key :calendar_exceptions, :schedules, column: :schedule_service_id 4 | remove_foreign_key :trips, :schedules, column: :schedule_service_id 5 | add_foreign_key :calendar_exceptions, :schedules, column: :schedule_service_id, primary_key: :service_id, on_update: :cascade, on_delete: :cascade 6 | add_foreign_key :trips, :schedules, column: :schedule_service_id, primary_key: :service_id, on_update: :cascade, on_delete: :cascade 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Webpack, along with any other files 2 | // present in this directory. You're encouraged to place your actual application logic in 3 | // a relevant structure within app/javascript and only use these pack files to reference 4 | // that code so it'll be compiled. 5 | 6 | import 'core-js/stable' 7 | import 'regenerator-runtime/runtime' 8 | import React from 'react'; 9 | import ReactDOM from 'react-dom'; 10 | import App from './components/app.jsx'; 11 | import WebpackerReact from 'webpacker-react' 12 | 13 | WebpackerReact.setup({App}) 14 | -------------------------------------------------------------------------------- /db/migrate/20230309061906_seed_gcm_connections.rb: -------------------------------------------------------------------------------- 1 | class SeedGcmConnections < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Connection.create!(from_stop_internal_id: "631", name: "LIRR", mode: "train", min_transfer_time: 300, access_time_from: 19800, access_time_to: 86399) 4 | Scheduled::Connection.create!(from_stop_internal_id: "723", name: "LIRR", mode: "train", min_transfer_time: 300, access_time_from: 19800, access_time_to: 86399) 5 | Scheduled::Connection.create!(from_stop_internal_id: "901", name: "LIRR", mode: "train", min_transfer_time: 300, access_time_from: 19800, access_time_to: 86399) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/packs/components/stationList.scss: -------------------------------------------------------------------------------- 1 | .station-list { 2 | .results .results-list-item { 3 | .cross { 4 | height: 21px; 5 | width: 21px; 6 | margin: .25em; 7 | margin-top: 0; 8 | } 9 | 10 | .ui.header { 11 | i.icon.pin, i.icon.location { 12 | display: inline; 13 | font-size: 1em; 14 | 15 | &:only-child { 16 | margin-right: 0.25rem; 17 | } 18 | } 19 | 20 | .secondary-name { 21 | color: #aaaaaa; 22 | font-weight: 300!important; 23 | margin-left: .4rem; 24 | font-size: 0.9em; 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/controllers/oauth_controller.rb: -------------------------------------------------------------------------------- 1 | require "uri" 2 | require "net/http" 3 | 4 | class OauthController < ApplicationController 5 | 6 | SLACK_OAUTH_URI = "https://slack.com/api/oauth.access" 7 | 8 | def index 9 | code = params[:code] 10 | 11 | post_params = { 12 | client_id: ENV["SLACK_CLIENT_ID"], 13 | client_secret: ENV["SLACK_CLIENT_SECRET"], 14 | code: code 15 | } 16 | data = Net::HTTP.post_form(URI.parse(SLACK_OAUTH_URI), post_params) 17 | 18 | redirect_to ENV["SLACK_REDIRECT_URI"], allow_other_host: true 19 | end 20 | 21 | def slack_install 22 | redirect_to ENV["SLACK_DIRECT_INSTALL_URI"], allow_other_host: true 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /app/models/scheduled/schedule.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::Schedule < ActiveRecord::Base 2 | has_many :calendar_exceptions, foreign_key: "schedule_service_id", primary_key: "service_id" 3 | scope :day, ->(day_of_the_week) { where("#{day_of_the_week} = 1")} 4 | 5 | def self.today(date: Date.current) 6 | joins("LEFT OUTER JOIN calendar_exceptions ON schedules.service_id ="\ 7 | "calendar_exceptions.schedule_service_id and calendar_exceptions.date = '#{date}'" 8 | ).where("(calendar_exceptions.exception_type = 1 or "\ 9 | "(#{Date::DAYNAMES[(date).wday].downcase} = 1 and calendar_exceptions.exception_type is null) and 10 | start_date <= '#{date}' and end_date >= '#{date}')") 11 | end 12 | end -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: postgresql 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: goodservice-v2_development 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: goodservice-v2_test 22 | 23 | production: 24 | url: <%= ENV['DATABASE_URL'] %> -------------------------------------------------------------------------------- /app/assets/images/cross-15.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/workers/scheduled_times_refresh_worker.rb: -------------------------------------------------------------------------------- 1 | class ScheduledTimesRefreshWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'default' 4 | 5 | def perform 6 | stop_pairs_map = {} 7 | Scheduled::Route.all.each do |route| 8 | Scheduled::Trip.soon_grouped(Time.current.to_i, route.id).each do |_, trips| 9 | trips.each do |trip| 10 | trip.stop_times.each_cons(2) do |a_st, b_st| 11 | time = b_st.departure_time - a_st.departure_time 12 | stop_pairs_map[[a_st.stop_internal_id, b_st.stop_internal_id]] = time 13 | end 14 | end 15 | end 16 | end 17 | 18 | REDIS_CLIENT.pipelined do |pipeline| 19 | stop_pairs_map.each do |(a_st, b_st), time| 20 | RedisStore.add_scheduled_travel_time(a_st, b_st, time, pipeline) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/javascript/packs/components/train.scss: -------------------------------------------------------------------------------- 1 | button.ui.inverted.secondary.button.train { 2 | color: white; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | height: 100%; 7 | 8 | .status { 9 | font-weight: 200; 10 | } 11 | 12 | .ui.header { 13 | color: white; 14 | } 15 | 16 | &:hover, &:active, .active, &:focus { 17 | background-color: rgba(0,0,0, 0.3); 18 | color: gray; 19 | 20 | .ui.header { 21 | color: gray; 22 | } 23 | } 24 | 25 | .train-bullet { 26 | float: left; 27 | 28 | .alternate-name { 29 | margin-left: .2em; 30 | text-align: left; 31 | } 32 | } 33 | 34 | @media only screen and (max-width: 767px) { 35 | padding: 0; 36 | border: none; 37 | background: none; 38 | min-width: 2em; 39 | } 40 | 41 | @media only screen and (min-width: 768px) { 42 | min-height: 8em; 43 | } 44 | } -------------------------------------------------------------------------------- /app/workers/travel_times_refresh_worker.rb: -------------------------------------------------------------------------------- 1 | class TravelTimesRefreshWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: false, queue: 'default' 4 | 5 | def perform 6 | routes = Scheduled::Route.all 7 | route_futures = {} 8 | 9 | REDIS_CLIENT.pipelined do |pipeline| 10 | route_futures = routes.to_h do |r| 11 | [r.internal_id, RedisStore.route_status(r.internal_id, pipeline)] 12 | end 13 | end 14 | 15 | stop_pairs = route_futures.flat_map { |_, rf| 16 | data = rf.value && JSON.parse(rf.value) 17 | data && data['actual_routings']&.flat_map do |_, routings| 18 | routings.flat_map { |r| r.each_cons(2).to_a } 19 | end 20 | }.compact.uniq 21 | 22 | travel_times = RouteProcessor.batch_average_travel_time_pairs(stop_pairs) 23 | data = Marshal.dump(travel_times) 24 | RedisStore.update_travel_times(data) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "goodservice-v2", 3 | "private": true, 4 | "engines": { 5 | "node": "14.x" 6 | }, 7 | "dependencies": { 8 | "@babel/preset-react": "^7.12.10", 9 | "@rails/actioncable": "^6.0.0", 10 | "@rails/activestorage": "^6.0.0", 11 | "@rails/ujs": "^6.0.0", 12 | "@rails/webpacker": "5.2.1", 13 | "es-cookie": "^1.3.2", 14 | "lodash": "^4.17.20", 15 | "react": "^17.0.1", 16 | "react-dom": "^17.0.1", 17 | "react-helmet": "^6.1.0", 18 | "react-hot-loader": "4", 19 | "react-router-dom": "^5.2.0", 20 | "resolve-url-loader": "^3.1.2", 21 | "semantic-ui-css": "^2.4.1", 22 | "semantic-ui-react": "^2.0.2", 23 | "turbolinks": "^5.2.0", 24 | "webpacker-react": "^0.3.2" 25 | }, 26 | "version": "0.1.0", 27 | "devDependencies": { 28 | "@babel/plugin-transform-react-jsx": "^7.12.12", 29 | "webpack-dev-server": "^3.11.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/javascript/packs/components/trainModalOverviewPane.scss: -------------------------------------------------------------------------------- 1 | .ui.segment.train-modal-overview-pane { 2 | @media only screen and (max-width: 767px) { 3 | padding-left: 0; 4 | padding-right: 0; 5 | } 6 | 7 | .ui.stackable.grid>.row>.column.map-cell { 8 | @media only screen and (max-width: 767px) { 9 | display: none; 10 | } 11 | } 12 | 13 | .ui.stackable.grid>.row>.column.mobile-map-cell { 14 | @media only screen and (min-width: 768px) { 15 | display: none; 16 | } 17 | } 18 | 19 | .ui.stackable.grid>.row>.column.status-cell { 20 | @media only screen and (max-width: 767px) { 21 | margin-left: -1rem !important; 22 | margin-right: -1rem !important; 23 | 24 | padding-left: 0 !important; 25 | padding-right: 0 !important; 26 | } 27 | } 28 | 29 | .ui.small.statistics .statistic>.value { 30 | @media only screen and (max-width: 767px) { 31 | font-size: 2rem!important; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/controllers/api/trips_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::TripsController < ApplicationController 2 | def show 3 | trip_id = params[:id].sub("-", "..") 4 | marshaled_trip = RedisStore.active_trip(FeedRetrieverSpawningWorkerBase.feed_id_for(params[:route_id]), trip_id) 5 | 6 | if !marshaled_trip && trip_id.include?("..") 7 | trip_id = trip_id.sub("..", ".") 8 | marshaled_trip = RedisStore.active_trip(FeedRetrieverSpawningWorkerBase.feed_id_for(params[:route_id]), trip_id) 9 | end 10 | 11 | raise ActionController::RoutingError.new('Not Found') unless marshaled_trip 12 | trip = Marshal.load(marshaled_trip) 13 | 14 | data = { 15 | route_id: trip.route_id, 16 | trip_id: trip.id, 17 | stop_times: trip.stops, 18 | tracks: trip.tracks, 19 | past_stops: trip.past_stops, 20 | is_assigned: trip.is_assigned, 21 | timestamp: trip.timestamp 22 | } 23 | 24 | expires_now 25 | render json: data 26 | end 27 | end -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Custom 8 | /import/*.txt 9 | /import/*.csv 10 | 11 | # Ignore bundler config. 12 | /.bundle 13 | 14 | # Ignore the default SQLite database. 15 | /db/*.sqlite3 16 | /db/*.sqlite3-* 17 | 18 | # Ignore all logfiles and tempfiles. 19 | /log/* 20 | /tmp/* 21 | !/log/.keep 22 | !/tmp/.keep 23 | 24 | # Ignore pidfiles, but keep the directory. 25 | /tmp/pids/* 26 | !/tmp/pids/ 27 | !/tmp/pids/.keep 28 | 29 | # Ignore uploaded files in development. 30 | /storage/* 31 | !/storage/.keep 32 | 33 | /public/assets 34 | .byebug_history 35 | 36 | # Ignore master key for decrypting credentials and more. 37 | /config/master.key 38 | 39 | /public/packs 40 | /public/packs-test 41 | /node_modules 42 | /yarn-error.log 43 | yarn-debug.log* 44 | .yarn-integrity 45 | 46 | .DS_Store 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sunny Ng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /db/migrate/20211002064045_seed_times_sq_to_bryant_pk_transfer.rb: -------------------------------------------------------------------------------- 1 | class SeedTimesSqToBryantPkTransfer < ActiveRecord::Migration[6.1] 2 | def change 3 | Scheduled::Transfer.create!(from_stop_internal_id: "D16", to_stop_internal_id: "902", min_transfer_time: 300, access_time_from: 21600, access_time_to: 86399) 4 | Scheduled::Transfer.create!(from_stop_internal_id: "D16", to_stop_internal_id: "R16", min_transfer_time: 420, access_time_from: 21600, access_time_to: 86399) 5 | Scheduled::Transfer.create!(from_stop_internal_id: "D16", to_stop_internal_id: "127", min_transfer_time: 420, access_time_from: 21600, access_time_to: 86399) 6 | Scheduled::Transfer.create!(from_stop_internal_id: "902", to_stop_internal_id: "D16", min_transfer_time: 300, access_time_from: 21600, access_time_to: 86399) 7 | Scheduled::Transfer.create!(from_stop_internal_id: "R16", to_stop_internal_id: "D16", min_transfer_time: 420, access_time_from: 21600, access_time_to: 86399) 8 | Scheduled::Transfer.create!(from_stop_internal_id: "127", to_stop_internal_id: "D16", min_transfer_time: 420, access_time_from: 21600, access_time_to: 86399) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20240520022419_use_official_mta_colors.rb: -------------------------------------------------------------------------------- 1 | class UseOfficialMtaColors < ActiveRecord::Migration[7.1] 2 | def change 3 | color_map = { 4 | '1' => 'ee352e', 5 | '2' => 'ee352e', 6 | '3' => 'ee352e', 7 | '4' => '00933c', 8 | '5' => '00933c', 9 | '6' => '00933c', 10 | '6X' => '00933c', 11 | '7' => 'b933ad', 12 | '7X' => 'b933ad', 13 | 'GS' => '6d6e71', 14 | 'A' => '2850ad', 15 | 'B' => 'ff6319', 16 | 'C' => '2850ad', 17 | 'D' => 'ff6319', 18 | 'E' => '2850ad', 19 | 'F' => 'ff6319', 20 | 'FX' => 'ff6319', 21 | 'FS' => '6d6e71', 22 | 'G' => '6cbe45', 23 | 'J' => '996633', 24 | 'L' => 'a7a9ac', 25 | 'M' => 'ff6319', 26 | 'N' => 'fccc0a', 27 | 'Q' => 'fccc0a', 28 | 'R' => 'fccc0a', 29 | 'H' => '6d6e71', 30 | 'W' => 'fccc0a', 31 | 'Z' => '996633', 32 | 'SI' => '00396a', 33 | } 34 | color_map.each do |route_id, color| 35 | route = Scheduled::Route.find_by!(internal_id: route_id) 36 | route.color = color 37 | route.save! 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module GoodserviceV2 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 7.1 13 | 14 | config.add_autoload_paths_to_load_path = false 15 | config.active_support.cache_format_version = 7.1 16 | 17 | # Configuration for the application, engines, and railties goes here. 18 | # 19 | # These settings can be overridden in specific environments using the files 20 | # in config/environments, which are processed later. 21 | # 22 | # config.time_zone = "Central Time (US & Canada)" 23 | # config.eager_load_paths << Rails.root.join("extras") 24 | config.time_zone = 'America/New_York' 25 | 26 | config.middleware.insert_before 0, Rack::Cors do 27 | allow do 28 | origins '*' 29 | resource '*', 30 | headers: :any, 31 | methods: %i(get) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies 21 | system! 'bin/yarn' 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/models/scheduled/stop_time.rb: -------------------------------------------------------------------------------- 1 | class Scheduled::StopTime < ActiveRecord::Base 2 | belongs_to :trip, foreign_key: "trip_internal_id", primary_key: "internal_id" 3 | belongs_to :stop, foreign_key: "stop_internal_id", primary_key: "internal_id" 4 | 5 | DAY_IN_MINUTES = 86400 6 | BUFFER = 10800 7 | 8 | def self.rounded_time 9 | Time.current.change(sec: 0) 10 | end 11 | 12 | def self.not_past(current_timestamp: rounded_time.to_i) 13 | current_time = Time.zone.at(current_timestamp) 14 | if (current_time + BUFFER).to_date == current_time.to_date.tomorrow 15 | where("departure_time > ? or (? - departure_time > ?)", 16 | current_time - current_time.beginning_of_day, 17 | current_time - current_time.beginning_of_day, 18 | BUFFER, 19 | ) 20 | elsif current_time.hour < 4 21 | where("(departure_time < ? and departure_time > ?) or (departure_time >= ? and departure_time - ? > ?)", 22 | DAY_IN_MINUTES - BUFFER, 23 | current_time - current_time.beginning_of_day, 24 | DAY_IN_MINUTES, 25 | DAY_IN_MINUTES, 26 | current_time - current_time.beginning_of_day 27 | ) 28 | else 29 | where("departure_time > ?", current_time - current_time.beginning_of_day) 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /app/workers/accessibility_list_worker.rb: -------------------------------------------------------------------------------- 1 | class AccessibilityListWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: 1, queue: 'low' 4 | 5 | FEED_URI = "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fnyct_ene_equipments.json" 6 | 7 | def perform 8 | feed_data = retrieve_feed 9 | update_accessible_stops_list(feed_data) 10 | end 11 | 12 | private 13 | 14 | def retrieve_feed 15 | uri = URI.parse(FEED_URI) 16 | Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| 17 | request = Net::HTTP::Get.new uri 18 | request["x-api-key"] = ENV["MTA_KEY"] 19 | 20 | response = http.request request 21 | JSON.parse(response.body).select { |h| h['equipmenttype'] == 'EL' && h['ADA'] == 'Y' } 22 | end 23 | end 24 | 25 | def update_accessible_stops_list(data) 26 | accessible_stations = Set.new 27 | elevator_map = {} 28 | 29 | data.each do |elevator| 30 | stations = elevator['elevatorsgtfsstopid'].split('/') 31 | stations.each do |s| 32 | accessible_stations << s 33 | end 34 | elevator_map[elevator['equipmentno']] = stations 35 | end 36 | 37 | RedisStore.update_accessible_stops_list(accessible_stations.to_a.to_json) 38 | RedisStore.update_elevator_map(elevator_map.to_json) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/workers/accessibility_statuses_worker.rb: -------------------------------------------------------------------------------- 1 | class AccessibilityStatusesWorker 2 | include Sidekiq::Worker 3 | sidekiq_options retry: 1, queue: 'low' 4 | 5 | FEED_URI = "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fnyct_ene.json" 6 | 7 | def perform 8 | feed_data = retrieve_feed 9 | update_elevator_advisories(feed_data) 10 | end 11 | 12 | private 13 | 14 | def retrieve_feed 15 | uri = URI.parse(FEED_URI) 16 | Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| 17 | request = Net::HTTP::Get.new uri 18 | request["x-api-key"] = ENV["MTA_KEY"] 19 | 20 | response = http.request request 21 | JSON.parse(response.body).select{ |s| s['equipmenttype'] == 'EL' && s['ADA'] == 'Y' && s['isupcomingoutage'] == 'N' } 22 | end 23 | end 24 | 25 | def update_elevator_advisories(data) 26 | elevator_map_json = RedisStore.elevator_map 27 | return unless elevator_map_json 28 | elevator_map = JSON.parse(elevator_map_json) 29 | 30 | advisories = Hash.new { |h, k| h[k] = [] } 31 | data.each do |status| 32 | elevator_map[status['equipment']]&.flatten&.each do |station| 33 | advisories[station] << HTMLEntities.new.decode(status['serving']) 34 | end 35 | end 36 | 37 | RedisStore.update_elevator_advisories(advisories.to_json) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/javascript/packs/components/trainModal.scss: -------------------------------------------------------------------------------- 1 | .ui.fullscreen.modal.active.train-modal { 2 | @media only screen and (max-width: 767px) { 3 | width: 100% !important; 4 | } 5 | 6 | .close.icon { 7 | color: white; 8 | } 9 | 10 | .header { 11 | border: 0; 12 | 13 | .ui.grid { 14 | @media only screen and (max-width: 767px) { 15 | margin-top: 1em; 16 | } 17 | 18 | .train-name-cell { 19 | @media only screen and (max-width: 767px) { 20 | padding-right: 0; 21 | } 22 | } 23 | 24 | .train-direction-nav-cell { 25 | @media only screen and (max-width: 767px) { 26 | padding-left: 0; 27 | } 28 | 29 | .ui.menu.header-menu { 30 | .item.no-service { 31 | text-decoration: line-through; 32 | color: #999999; 33 | } 34 | 35 | @media only screen and (max-width: 767px) { 36 | margin-left: 1em!important; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | .scrolling.content { 44 | padding-top: 0!important; 45 | 46 | .description { 47 | padding-top: 0!important; 48 | } 49 | } 50 | 51 | &.blurring { 52 | filter: blur(5px) grayscale(.7); 53 | } 54 | 55 | .ui.basic.inverted.segment { 56 | .ui.yellow.top.attached.label { 57 | color: black !important; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /db/migrate/20201229201218_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration[6.1] 2 | def self.up 3 | create_table :delayed_jobs do |table| 4 | table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. 6 | table.text :handler, null: false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps null: true 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /public/.well-known/apple-app-site-association: -------------------------------------------------------------------------------- 1 | { 2 | "applinks": { 3 | "apps": [], 4 | "details": [ 5 | { 6 | "appIDs": ["LT5JAC55XG.io.goodservice.The-Weekendest"], 7 | "components": [ 8 | { 9 | "/": "/*", 10 | "comment": "Matches any URL" 11 | }, 12 | { 13 | "/": "/api/*", 14 | "exclude": true, 15 | "comment": "Exclude APIs" 16 | }, 17 | { 18 | "/": "/about", 19 | "exclude": true, 20 | "comment": "Exclude About" 21 | }, 22 | { 23 | "/": "/twitter", 24 | "exclude": true, 25 | "comment": "Exclude Twitter" 26 | }, 27 | { 28 | "/": "/slack", 29 | "exclude": true, 30 | "comment": "Exclude Slack" 31 | }, 32 | { 33 | "/": "/slack/*", 34 | "exclude": true, 35 | "comment": "Exclude Slack" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /app/models/service_changes/split_routing_service_change.rb: -------------------------------------------------------------------------------- 1 | class ServiceChanges::SplitRoutingServiceChange < ServiceChanges::ServiceChange 2 | attr_accessor :routing_tuples, :related_routes_by_segments 3 | 4 | def initialize(direction, routing_tuples, long_term_override = false) 5 | self.direction = direction 6 | self.affects_some_trains = false 7 | self.routing_tuples = routing_tuples 8 | self.related_routes_by_segments = {} 9 | self.long_term_override = long_term_override 10 | end 11 | 12 | def match?(comparing_change) 13 | routing_tuples.size == comparing_change.routing_tuples.size && 14 | routing_tuples.all? { |r| comparing_change.routing_tuples.any? { |c| c.first == r.last && c.last == r.first }} 15 | end 16 | 17 | def begin_of_route? 18 | false 19 | end 20 | 21 | def end_of_route? 22 | false 23 | end 24 | 25 | def applicable_to_routing?(_) 26 | true 27 | end 28 | 29 | def hash 30 | self.class.hash ^ self.direction.hash ^ self.routing_tuples.hash 31 | end 32 | 33 | def ==(other) 34 | self.class == other.class && self.direction == other.direction && self.routing_tuples == other.routing_tuples 35 | end 36 | 37 | def as_json(options = {}) 38 | { 39 | type: self.class.name.demodulize, 40 | stations_affected: stations_affected, 41 | related_routes: related_routes, 42 | routing_tuples: routing_tuples, 43 | long_term: !not_long_term?, 44 | } 45 | end 46 | end -------------------------------------------------------------------------------- /lib/nyct-subway.pb.rb: -------------------------------------------------------------------------------- 1 | require 'protobuf' 2 | require 'google/transit/gtfs-realtime.pb' 3 | 4 | module Transit_realtime 5 | 6 | # forward declarations 7 | class TripReplacementPeriod < ::Protobuf::Message; end 8 | class NyctFeedHeader < ::Protobuf::Message; end 9 | class NyctTripDescriptor < ::Protobuf::Message; end 10 | class NyctStopTimeUpdate < ::Protobuf::Message; end 11 | 12 | class TripReplacementPeriod < ::Protobuf::Message 13 | optional :string, :route_id, 1 14 | optional TimeRange, :replacement_period, 2 15 | end 16 | 17 | class NyctFeedHeader < ::Protobuf::Message 18 | required :string, :nyct_subway_version, 1 19 | repeated TripReplacementPeriod, :trip_replacement_period, 2 20 | end 21 | 22 | class NyctTripDescriptor < ::Protobuf::Message 23 | # forward declarations 24 | 25 | # enums 26 | class Direction < ::Protobuf::Enum 27 | define :NORTH, 1 28 | define :EAST, 2 29 | define :SOUTH, 3 30 | define :WEST, 4 31 | end 32 | 33 | optional :string, :train_id, 1 34 | optional :bool, :is_assigned, 2 35 | optional Direction, :direction, 3 36 | end 37 | 38 | class TripDescriptor 39 | optional NyctTripDescriptor, :nyct_trip_descriptor, 1001 40 | end 41 | 42 | class NyctStopTimeUpdate < ::Protobuf::Message 43 | optional :string, :scheduled_track, 1 44 | optional :string, :actual_track, 2 45 | end 46 | 47 | class TripUpdate::StopTimeUpdate 48 | optional NyctStopTimeUpdate, :nyct_stop_time_update, 1001 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | # # If you are using webpack-dev-server then specify webpack-dev-server host 15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 16 | 17 | # # Specify URI for violation reports 18 | # # policy.report_uri "/csp-violation-report-endpoint" 19 | # end 20 | 21 | # If you are using UJS then enable automatic nonce generation 22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 23 | 24 | # Set the nonce only to specific directives 25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 26 | 27 | # Report CSP violations to a specified URI 28 | # For further information see the following documentation: 29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 30 | # Rails.application.config.content_security_policy_report_only = true 31 | -------------------------------------------------------------------------------- /app/javascript/packs/components/trainBullet.scss: -------------------------------------------------------------------------------- 1 | .route { 2 | width: 1.5em; 3 | height: 1.5em; 4 | font-size: 2.25em; 5 | line-height: 1.5em; 6 | text-align: center; 7 | font-weight: bold; 8 | font-style: normal!important; 9 | color: #fff; 10 | margin: .25em; 11 | 12 | &.medium { 13 | height: 1.75em; 14 | width: 1.75em; 15 | font-size: 1.75em; 16 | line-height: 1.75em; 17 | } 18 | 19 | &.small { 20 | height: 1.5em; 21 | width: 1.5em; 22 | font-size: 1em; 23 | line-height: 1.5em; 24 | display: inline-block; 25 | } 26 | 27 | &.condensed { 28 | font-stretch: condensed; 29 | } 30 | } 31 | 32 | .bullet { 33 | -webkit-border-radius: 999px; 34 | -moz-border-radius: 999px; 35 | border-radius: 999px; 36 | font-family: Helvetica Neue, Helvetica, Arial, Sans-serif, Open Sans; 37 | 38 | &.downtown-only { 39 | border-top-left-radius: 0; 40 | border-top-right-radius: 0; 41 | border-bottom-left-radius: .8em; 42 | border-bottom-right-radius: .8em; 43 | height: 0.8em; 44 | line-height: 0.1em; 45 | } 46 | 47 | &.uptown-only { 48 | border-top-left-radius: .8em; 49 | border-top-right-radius: .8em; 50 | border-bottom-left-radius: 0; 51 | border-bottom-right-radius: 0; 52 | height: 0.8em; 53 | } 54 | } 55 | 56 | .diamond { 57 | transform:rotate(45deg); 58 | -ms-transform:rotate(45deg); 59 | -webkit-transform:rotate(45deg); 60 | } 61 | 62 | .diamond-inner { 63 | transform:rotate(-45deg); 64 | -ms-transform:rotate(-45deg); 65 | -webkit-transform:rotate(-45deg); 66 | } -------------------------------------------------------------------------------- /app/javascript/packs/components/tripModal.scss: -------------------------------------------------------------------------------- 1 | .ui.large.modal.active.trip-modal { 2 | @media only screen and (max-width: 767px) { 3 | width: 100% !important; 4 | } 5 | 6 | .modal-header { 7 | @media only screen and (max-width: 767px) { 8 | font-size: 1.2em; 9 | } 10 | 11 | .route { 12 | float: left; 13 | } 14 | 15 | .trip-header-info { 16 | float: left; 17 | margin-top: .5em; 18 | 19 | .delayed-header { 20 | margin-top: 0; 21 | } 22 | } 23 | 24 | .past-stops-toggle { 25 | float: right; 26 | clear: both; 27 | 28 | @media only screen and (min-width: 768px) { 29 | margin-right: 1em; 30 | } 31 | 32 | .toggle-label { 33 | color: #ccc !important; 34 | } 35 | 36 | .ui.toggle.checkbox input:checked~label.toggle-label, .ui.toggle.checkbox input:focus:checked~label.toggle-label { 37 | color: #ccc !important; 38 | } 39 | 40 | .ui.toggle.checkbox .box:before, .ui.toggle.checkbox label.toggle-label:before, .ui.toggle.checkbox input:focus~.box:before, .ui.toggle.checkbox input:focus~label.toggle-label:before, .ui.toggle.checkbox .box:hover::before, .ui.toggle.checkbox label.toggle-label:hover::before { 41 | background: rgba(255,255,255,.05) 42 | } 43 | } 44 | } 45 | 46 | .description { 47 | margin-top: 0; 48 | padding-top: 0!important; 49 | 50 | .divider { 51 | margin-top: 0; 52 | } 53 | 54 | .trip-table { 55 | .past { 56 | color: #aaaaaa; 57 | } 58 | .delayed { 59 | color: #ff695e; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | require "sidekiq/web" # require the web UI 2 | require "sidekiq/cron/web" # require cron tab UI 3 | 4 | Rails.application.routes.draw do 5 | Sidekiq::Web.use Rack::Auth::Basic do |username, password| 6 | # Protect against timing attacks: 7 | # - See https://codahale.com/a-lesson-in-timing-attacks/ 8 | # - See https://thisdata.com/blog/timing-attacks-against-string-comparison/ 9 | # - Use & (do not use &&) so that it doesn't short circuit. 10 | # - Use digests to stop length information leaking (see also ActiveSupport::SecurityUtils.variable_size_secure_compare) 11 | ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USERNAME"])) & 12 | ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"])) 13 | end if Rails.env.production? 14 | mount Sidekiq::Web, at: "/sidekiq" 15 | namespace :api do 16 | resources :routes, only: [:index, :show] do 17 | resources :trips, only: [:show] 18 | end 19 | resources :stops, only: [:index, :show] 20 | post '/slack', to: 'slack#index' 21 | post '/slack/query', to: 'slack#query' 22 | post '/alexa', to: 'alexa#index' 23 | get '/stats', to: 'stats#index' 24 | end 25 | get '/about', to: 'index#index' 26 | get '/twitter', to: 'index#index' 27 | get '/trains(/*id)', to: 'index#index' 28 | get '/stations(/*id)', to: 'index#index' 29 | get '/oauth', to: 'oauth#index' 30 | get '/slack', to: 'slack#index' 31 | get '/slack/help', to: 'slack#help' 32 | get '/slack/privacy', to: 'slack#privacy' 33 | get '/slack/install', to: 'oauth#slack_install' 34 | root 'index#index' 35 | end 36 | -------------------------------------------------------------------------------- /app/javascript/packs/components/accessibility.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from "semantic-ui-react"; 3 | 4 | import './accessibility.scss'; 5 | 6 | export const accessibilityIcon = (accessibility) => { 7 | if (!accessibility) { 8 | return; 9 | } 10 | 11 | const accessibleNorth = accessibility.directions.includes('north'); 12 | const accessibleSouth = accessibility.directions.includes('south'); 13 | 14 | if (accessibility.advisories.length > 0) { 15 | return ( 16 | 17 |If you are the application owner check the logs for more information.
64 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |
71 | Last updated {timestamp && (new Date(timestamp * 1000)).toLocaleTimeString('en-US')}.
72 | Source code.
73 | Subway Route Symbols ®: Metropolitan Transportation Authority. Used with permission.
74 |
30 | goodservice.io's goal is to provide an up-to-date and detailed view of the New York City subway system 31 | using the publicly available GTFS and GTFS-RT data. It is an open source project, and 32 | the source code can be found on GitHub. 33 | Currently, it displays live route maps, maximum wait times (i.e. train headways or 34 | frequency), train delays, and estimated train arrival times based on past train departure times. 35 |
36 |37 | The statuses displayed are defined as follow (in the order of how they are assigned): 38 |
50 | The Good Service Blog is where you can find me writing about transit, 51 | but mostly about this site. Some highlights: 52 | 53 |
Other transit projects I've been working on: 63 | 64 |
goodservice.io does not want to know who you are and does not store any of your personal information.
13 |During your use of goodservice.io, we do not require you to provide us with any personal data.
15 |We are given your team's Slack ID, Team Name and Domain upon registration and upon each command call, but we do not store any of it.
18 |We do not require that you provide us with any personal data to enable functionality of goodservice.io.
20 |We collect and log a minimal amount of activity data related to interactions had with goodservice.io via Google Analytics. This includes but is not limited to, Location request, webpages accessed, IP addressed, browser types, and operating system. Please refer to Google Analytics' Privacy & Terms on how Google Analytics uses the information.
22 |We shall not sell, transfer or lease out your personal data to third parties.
24 |We reserve the right, at our sole discretion, to modify this Privacy policy or any portion thereof. Any changes will be effective from the time of publication of the new Privacy policy. Your use of the Website after the changes have been implemented implicitly expresses your acknowledgement and acceptance of the new Privacy policy. Otherwise, and if the new Privacy policy does not suit you, you must no longer use the Services.
26 |We reserve the right, at our sole discretion, to modify this Privacy policy or any portion thereof. Any changes will be effective from the time of publication of the new Privacy policy. Your use of the Website after the changes have been implemented implicitly expresses your acknowledgement and acceptance of the new Privacy policy. Otherwise, and if the new Privacy policy does not suit you, you must no longer use the Services.
28 |