373 lines
11 KiB
JavaScript
373 lines
11 KiB
JavaScript
import React, { Component } from 'react';
|
|
import { BrowserRouter, Route, Link } from "react-router-dom";
|
|
import _ from 'lodash';
|
|
import { HeaderBar } from "./lib/header-bar.js"
|
|
|
|
function padNumber(number) {
|
|
if (number == 0) {
|
|
return "00";
|
|
}
|
|
if (number <= 9) {
|
|
return `0${number}`
|
|
}
|
|
return number.toString();
|
|
}
|
|
|
|
function isSundaySchedule(curTime) {
|
|
// Deliberately switch over the effective day in the middle of the
|
|
// night.
|
|
const dayOfWeek = curTime.getDay();
|
|
const hour = curTime.getHours();
|
|
const isSunday = (dayOfWeek === 0 && hour > 4) || (dayOfWeek === 1 && hour < 4);
|
|
return isSunday;
|
|
}
|
|
|
|
function getOptionsFromStations(stations) {
|
|
return _.map(stations, (station) => {
|
|
const abbr = station.abbr;
|
|
const name = station.name;
|
|
return <option key={abbr} value={abbr}>{name}</option>;
|
|
});
|
|
}
|
|
|
|
class ScheduleWidget extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { station: "" };
|
|
|
|
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
if (state.station === "" && props.stations && props.stations[0]) {
|
|
const abbr = props.stations[0].abbr;
|
|
return { station: abbr }
|
|
}
|
|
return null;
|
|
}
|
|
getSchedule(evt) {
|
|
// Needs to make a request at https://www.bart.gov/schedules/bystationresults?station=12TH&date=06/03/2020&time=6%3A30%20PM
|
|
const station = this.state.station;
|
|
const t = new Date();
|
|
const date = `${t.getMonth()}/${t.getDay()}/${t.getYear()}`
|
|
|
|
const hours = t.getHours();
|
|
const h = hours === 0 ? 12 : hours % 12;
|
|
const m = padNumber(t.getMinutes());
|
|
const meridian = hours >= 12 ? "PM": "AM"
|
|
const timeStr = `${h}:${m} ${meridian}`;
|
|
const url = `https://www.bart.gov/schedules/bystationresults?station=${station}&date=${date}&time=${timeStr}`;
|
|
window.open(url, '_blank');
|
|
evt.preventDefault();
|
|
}
|
|
|
|
changeStation(evt) {
|
|
const value = evt.target.value;
|
|
this.setState({station: value});
|
|
}
|
|
|
|
render() {
|
|
const stations = this.props.stations;
|
|
return (<div>
|
|
<form name="getSchedule" onSubmit={this.getSchedule.bind(this)}>
|
|
<select disabled={!stations} name="stations" value={this.state.fromStation} onChange={this.changeStation.bind(this)}>
|
|
{ getOptionsFromStations(stations) }
|
|
</select>
|
|
<input type="submit" value="Get schedule"/>
|
|
</form>
|
|
</div>);
|
|
}
|
|
}
|
|
|
|
class ElevatorWidget extends Component {
|
|
|
|
statuses() {
|
|
const elevatorStatuses = this.props.elevatorStatuses;
|
|
if (elevatorStatuses.length === 0) {
|
|
return <p>No elevators are known to be out of service.</p>;
|
|
}
|
|
return (<div>
|
|
{ _.map(elevatorStatuses, (st, idx) => {
|
|
const desc = st.description['#cdata-section'];
|
|
return <p key={idx}>{desc}</p>;
|
|
})
|
|
}
|
|
</div>);
|
|
}
|
|
|
|
render() {
|
|
return (<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden">
|
|
<h1 className="mt0 f8 fw4">BART Info - Elevator status</h1>
|
|
<Link to="/~bartinfo">Route planner</Link>
|
|
{ this.statuses() }
|
|
</div>);
|
|
}
|
|
}
|
|
|
|
class TimeScheduleWidget extends Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.state = { curTime: new Date() };
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.timerId = setInterval(() => this.tick(), 1000);
|
|
}
|
|
componentWillUnmount() {
|
|
clearInterval(this.timerId);
|
|
}
|
|
|
|
tick() {
|
|
this.setState({curTime: new Date()});
|
|
}
|
|
|
|
render() {
|
|
const curTime = this.state.curTime;
|
|
const timeStr = curTime.toLocaleTimeString();
|
|
const serviceStr = isSundaySchedule(curTime) ? "Sunday service" : "Weekday / Saturday service";
|
|
return (<div style={{textAlign: "center"}}>
|
|
<p className="lh-copy measure pt3">Current time: <b>{timeStr}</b></p>
|
|
<p className="lh-copy measure pt3">{serviceStr}</p>
|
|
</div>);
|
|
}
|
|
}
|
|
|
|
class RouteSearch extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
const now = new Date();
|
|
const hours = now.getHours();
|
|
this.state = {
|
|
fromStation: "",
|
|
toStation: "",
|
|
depart: 'now',
|
|
min: now.getMinutes(),
|
|
hour: hours === 0 ? 12 : hours % 12,
|
|
isPM: hours >= 12
|
|
};
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
if (state.fromStation === "" && props.stations && props.stations[0]) {
|
|
const abbr = props.stations[0].abbr;
|
|
return { ...state, fromStation: abbr, toStation: abbr};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
stationSearch(evt) {
|
|
evt.preventDefault();
|
|
api.action("bartinfo", "json", {
|
|
from: this.state.fromStation,
|
|
to: this.state.toStation,
|
|
min: this.state.min,
|
|
hour: this.state.hour,
|
|
isPM: this.state.isPM,
|
|
});
|
|
}
|
|
|
|
changeStation(evt) {
|
|
const target = evt.target.name;
|
|
const value = evt.target.value;
|
|
if (target === "fromStation") {
|
|
this.setState({fromStation: value});
|
|
} else if (target === "toStation") {
|
|
this.setState({toStation: value});
|
|
}
|
|
}
|
|
|
|
setDepartNow(evt) {
|
|
evt.preventDefault();
|
|
this.setState({depart: "now"});
|
|
}
|
|
|
|
setDepartAt(evt) {
|
|
evt.preventDefault();
|
|
const now = new Date();
|
|
const hours = now.getHours();
|
|
this.setState({
|
|
depart: "givenTime",
|
|
min: now.getMinutes(),
|
|
hour: hours === 0 ? 12 : hours % 12,
|
|
isPM: hours >= 12
|
|
});
|
|
}
|
|
|
|
renderTimePicker() {
|
|
const state = this.state;
|
|
const departNow = this.state.depart === 'now';
|
|
return (<div style={{display: "flex"}}>
|
|
<div>
|
|
<a href="" onClick={ this.setDepartNow.bind(this) }>
|
|
<div>
|
|
<p>{ departNow ? <b>Now</b> : "Now" }</p>
|
|
</div>
|
|
</a>
|
|
<a href="" onClick={ this.setDepartAt.bind(this)}>
|
|
<div>
|
|
<p>{ departNow ? "At..." : <b>At...</b>}</p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<div></div>
|
|
<div>
|
|
</div>
|
|
<select
|
|
name="hour"
|
|
value={this.state.hour}
|
|
onChange={(evt) => this.setState({hour: parseInt(evt.target.value)}) } disabled={departNow}
|
|
>
|
|
{ _.map(_.range(1, 13), (hour) => { return <option key={`h-${hour}`} value={hour}>{padNumber(hour)}</option>;}) }
|
|
</select>
|
|
<span>:</span>
|
|
<select
|
|
name="min"
|
|
value={this.state.min}
|
|
onChange={(evt) => this.setState({min: parseInt(evt.target.value)}) } disabled={departNow}
|
|
>
|
|
{ _.map(_.range(0, 60), (min) => { return <option key={`m-${min}`} value={min}>{padNumber(min)}</option>;}) }
|
|
</select>
|
|
<select
|
|
name="isPM"
|
|
value={this.state.isPM ? "PM" : "AM"}
|
|
disabled={departNow}
|
|
onChange={(evt) => this.setState({isPM: evt.target.value === "PM"})}
|
|
>
|
|
<option value="AM">AM</option>
|
|
<option value="PM">PM</option>
|
|
</select>
|
|
</div>
|
|
</div>);
|
|
}
|
|
|
|
render() {
|
|
const receivedStations = this.props.stations;
|
|
return (<form name="bartSearch" onSubmit={this.stationSearch.bind(this)}>
|
|
From:
|
|
<select disabled={!receivedStations} name="fromStation" value={this.state.fromStation} onChange={this.changeStation.bind(this)}>
|
|
{ getOptionsFromStations(receivedStations) }
|
|
</select>
|
|
<br/>
|
|
To:
|
|
<select disabled={!receivedStations} name="toStation" value={this.state.toStation} onChange={this.changeStation.bind(this)}>
|
|
{ getOptionsFromStations(receivedStations) }
|
|
</select>
|
|
<div>
|
|
Depart at:
|
|
{ this.renderTimePicker() }
|
|
</div>
|
|
<div style={{padding: '5px'}}>
|
|
<input type="submit" value="Search"/>
|
|
</div>
|
|
</form>);
|
|
}
|
|
}
|
|
|
|
class IndividualRouteResult extends Component {
|
|
render() {
|
|
const trip = this.props.trip;
|
|
return (<div>
|
|
Depart: {trip.depart} Arrive: {trip.arrive} ({trip.time})
|
|
<br/>
|
|
Cost: {trip.fare}
|
|
<br/>
|
|
Legs:
|
|
{ _.map(trip.legs, (leg) => `${leg.line} line`) }
|
|
</div>);
|
|
}
|
|
}
|
|
|
|
class RouteResults extends Component {
|
|
render() {
|
|
const routes = this.props.routes;
|
|
console.log(this.props.routes);
|
|
if (!routes) {
|
|
return (<div></div>);
|
|
}
|
|
|
|
const request = routes.request;
|
|
const trip = request.trip;
|
|
const trips = _.map(trip, (t) => {
|
|
return {
|
|
fare: t['@fare'],
|
|
depart: t['@origTimeMin'],
|
|
arrive: t['@destTimeMin'],
|
|
time: t['@tripTime'],
|
|
legs: _.map(t.leg, (leg) => {
|
|
return {line: leg['@trainHeadStation'] };
|
|
})
|
|
};
|
|
});
|
|
|
|
return (<div>
|
|
Trains:
|
|
<br/>
|
|
{ _.map(trips, (trip, idx) => <IndividualRouteResult key={idx} trip={trip} />) }
|
|
</div>);
|
|
}
|
|
}
|
|
|
|
class RoutePlanner extends Component {
|
|
render() {
|
|
const curTime = this.props.curTime;
|
|
const mapFilename = "BART-system-map.png";
|
|
const mapPath=`/~bartinfo/img/${mapFilename}`;
|
|
|
|
return (
|
|
<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden">
|
|
<h1 className="mt0 f8 fw4">BART Info</h1>
|
|
<Link to="/~bartinfo/elevators">Elevator information</Link>
|
|
<div style={{display: "grid", gridTemplateColumns: "66% 34%", gridGap: "10px" }}>
|
|
<div className="topinfo" style={{gridColumn: "1 / 2"}}>
|
|
<TimeScheduleWidget/>
|
|
</div>
|
|
<div className="map" style={{gridColumn: "1", gridRow: "2"}}>
|
|
<img src={mapPath} />
|
|
</div>
|
|
<div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}>
|
|
Search scheduled trains:
|
|
<RouteSearch stations={this.props.stations} curTime={curTime} />
|
|
<br/>
|
|
<RouteResults routes={this.props.routes} />
|
|
or see the official bart scheduler for a given station, date and time:
|
|
<ScheduleWidget stations={this.props.stations}/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
export class Root extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = store.state;
|
|
store.setStateHandler((newState) => {
|
|
this.setState(newState);
|
|
});
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<BrowserRouter>
|
|
<div className="absolute h-100 w-100 bg-gray0-d ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl">
|
|
<HeaderBar/>
|
|
<Route exact path="/~bartinfo/elevators" render={ () =>
|
|
<ElevatorWidget
|
|
elevatorStatuses={this.state.elevators || []}
|
|
/> }/>
|
|
<Route exact path="/~bartinfo" render={ () =>
|
|
<RoutePlanner
|
|
curTime={new Date() }
|
|
stations={this.state.stations || []}
|
|
routes={this.state.routes}
|
|
/> } />
|
|
</div>
|
|
</BrowserRouter>
|
|
)
|
|
}
|
|
}
|
|
|