Compare commits

...

15 Commits

Author SHA1 Message Date
ronreg-ribdev
84db0687c2 Add README 2020-06-28 14:18:20 -07:00
ronreg-ribdev
9327f750f6 Update map 2020-06-27 01:09:30 -07:00
ronreg-ribdev
4437eb0794 Add link to open official BART schedule 2020-06-05 04:38:25 -07:00
ronreg-ribdev
bb0852ed97 Basic search functionalty works! 2020-06-05 04:09:57 -07:00
ronreg-ribdev
ed70fce6a7 RouteResults widget 2020-06-05 03:42:01 -07:00
ronreg-ribdev
c718c49ceb More json cleanup 2020-06-05 03:35:12 -07:00
ronreg-ribdev
0293b9c6ee Cleaning up a bunch of json-parsing 2020-06-05 03:27:36 -07:00
ronreg-ribdev
f10946b96d Do some js parsing 2020-06-05 02:42:25 -07:00
ronreg-ribdev
c538f145b1 Fix some bugs 2020-06-05 01:28:07 -07:00
ronreg-ribdev
e4bd7d3ced Big chunk of work 2020-06-04 03:57:18 -07:00
ronreg-ribdev
701a55ce29 Add time picker 2020-06-04 01:30:34 -07:00
ronreg-ribdev
ec47ad1622 Generate url for routeplan 2020-06-03 04:34:25 -07:00
ronreg-ribdev
656374a2fb Schedule picker widget 2020-06-03 03:43:46 -07:00
ronreg-ribdev
c329543e66 Fix json, handle routes response 2020-06-03 03:30:21 -07:00
ronreg-ribdev
7502be1c5e StationPicker sends json message 2020-06-03 03:22:09 -07:00
8 changed files with 339 additions and 54 deletions

View File

@ -1,5 +1,38 @@
BART runner app for [Urbit](http://urbit.org). BART (Bay Area Rapid Transit) landscape app for [Urbit](http://urbit.org).
The Bart App Map was created by Trucy Phan (https://github.com/trucy/bart-map) and is used under The Bart App Map was created by Trucy Phan (https://github.com/trucy/bart-map) and is used under
the terms of the Creative Commons Attribution 3.0 Unported License. the terms of the Creative Commons Attribution 3.0 Unported License.
# Installation
This app is based off of the [create-landscape-app](https://github.com/urbit/create-landscape-app) scaffolding.
To install, first boot your ship, and mount its pier using `|mount %` in the Dojo.
Then clone this repo, and create a file called `.urbitrc` at the root of the repo directory
with the following contents:
```
module.exports = {
URBIT_PIERS: [
"/path/to/ship/home",
]
};
```
For instance, if the repo was cloned into the same directory as a planet with a pier
`zod`, you might make the path `../zod/home`.
Then run the following Unix commands from the root of the repo:
```
$ yarn
$ yarn run build
```
This will build and package the javascript files, and move them into the directory
specified in the `.urbitrc` file.
Finally, run `|commit %home` in your ship's Dojo to make Urbit aware of those files,
and then run `|start %barttile` to start the app. You should then see a `BART info`
tile on your Landscape home screen.

View File

@ -3,6 +3,16 @@ import { BrowserRouter, Route, Link } from "react-router-dom";
import _ from 'lodash'; import _ from 'lodash';
import { HeaderBar } from "./lib/header-bar.js" 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) { function isSundaySchedule(curTime) {
// Deliberately switch over the effective day in the middle of the // Deliberately switch over the effective day in the middle of the
// night. // night.
@ -12,6 +22,63 @@ function isSundaySchedule(curTime) {
return isSunday; 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 { class ElevatorWidget extends Component {
statuses() { statuses() {
@ -65,17 +132,38 @@ class TimeScheduleWidget extends Component {
} }
} }
class RoutePlanner extends Component { class RouteSearch extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
const now = new Date();
const hours = now.getHours();
this.state = { this.state = {
fromStation: null, fromStation: "",
toStation: null, toStation: "",
depart: 'now',
min: now.getMinutes(),
hour: hours === 0 ? 12 : hours % 12,
isPM: hours >= 12
}; };
} }
stationSearch() { static getDerivedStateFromProps(props, state) {
console.log("Searching"); 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) { changeStation(evt) {
@ -88,33 +176,142 @@ class RoutePlanner extends Component {
} }
} }
renderStationOptions() { setDepartNow(evt) {
const stations = this.props.stations; evt.preventDefault();
return _.map(stations, (station) => { this.setState({depart: "now"});
const abbr = station.abbr; }
const name = station.name;
return <option key={abbr} value={abbr}>{name}</option>; 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
}); });
} }
renderStationForm() { renderTimePicker() {
return (<form name="bartSearch" onSubmit={this.stationSearch.bind(this)}> const state = this.state;
From: const departNow = this.state.depart === 'now';
<select name="fromStation" value={this.state.fromStation || ""} onChange={this.changeStation.bind(this)}> return (<div style={{display: "flex"}}>
{ this.renderStationOptions() } <div>
</select> <a href="" onClick={ this.setDepartNow.bind(this) }>
<br/> <div>
To: <p>{ departNow ? <b>Now</b> : "Now" }</p>
<select name="toStation" value={this.state.toStation || ""} onChange={this.changeStation.bind(this)}> </div>
{ this.renderStationOptions() } </a>
</select> <a href="" onClick={ this.setDepartAt.bind(this)}>
<input type="submit" value="Search"/> <div>
</form>); <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() { render() {
const curTime = this.props.curTime; const curTime = this.props.curTime;
const mapFilename = isSundaySchedule(curTime) ? "BART-Map-Sunday.png" : "BART-Map-Weekday-Saturday.png"; const mapFilename = "BART-system-map.png";
const mapPath=`/~bartinfo/img/${mapFilename}`; const mapPath=`/~bartinfo/img/${mapFilename}`;
return ( return (
@ -130,7 +327,11 @@ class RoutePlanner extends Component {
</div> </div>
<div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}> <div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}>
Search scheduled trains: Search scheduled trains:
{ this.renderStationForm() } <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> </div>
</div> </div>
@ -161,6 +362,7 @@ export class Root extends Component {
<RoutePlanner <RoutePlanner
curTime={new Date() } curTime={new Date() }
stations={this.state.stations || []} stations={this.state.stations || []}
routes={this.state.routes}
/> } /> /> } />
</div> </div>
</BrowserRouter> </BrowserRouter>

View File

@ -14,6 +14,11 @@ export class UpdateReducer {
if (elevators) { if (elevators) {
state.elevators = elevators; state.elevators = elevators;
} }
const routes = _.get(data, "routes", false);
if (routes) {
state.routes = routes;
}
} }
} }
} }

View File

@ -14,11 +14,9 @@ export class Subscription {
} }
initializebartinfo() { initializebartinfo() {
/* api.bind("/routes", "PUT", api.authTokens.ship, "bartinfo",
api.bind('/primary', 'PUT', api.authTokens.ship, 'bartinfo',
this.handleEvent.bind(this), this.handleEvent.bind(this),
this.handleError.bind(this)); this.handleError.bind(this));
*/
api.bind("/elevators", "PUT", api.authTokens.ship, "bartinfo", api.bind("/elevators", "PUT", api.authTokens.ship, "bartinfo",
this.handleEvent.bind(this), this.handleEvent.bind(this),

View File

@ -60,6 +60,10 @@
%+ give-simple-payload:app eyre-id %+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request %+ require-authorization:app inbound-request
poke-handle-http-request:cc poke-handle-http-request:cc
%json
=+ !<(jon=json vase)
:_ this
(poke-handle-json:cc jon)
:: ::
== ==
:: ::
@ -79,6 +83,8 @@
=/ req bart-api-elevator-status:cc =/ req bart-api-elevator-status:cc
[%pass /elevators %arvo %i %request req out] [%pass /elevators %arvo %i %request req out]
[~[elevator-status-request] this] [~[elevator-status-request] this]
?: ?=([%routes *] path)
[[~] this]
?: ?=([%http-response *] path) ?: ?=([%http-response *] path)
`this `this
?. =(/ path) ?. =(/ path)
@ -99,12 +105,18 @@
?> ?=(%o -.value) ?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~]) =/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/bartstations] %json !>(update)]~ [%give %fact ~[/bartstations] %json !>(update)]~
:: ::
[%elevators *] [%elevators *]
=/ value=json (parse-elevator-status-response:cc client-response.sign-arvo) =/ value=json (parse-elevator-status-response:cc client-response.sign-arvo)
?> ?=(%o -.value) ?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~]) =/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/elevators] %json !>(update)]~ [%give %fact ~[/elevators] %json !>(update)]~
::
[%routeplan *]
=/ value=json (parse-routeplan-response:cc client-response.sign-arvo)
?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/routes] %json !>(update)]~
== ==
[http-moves this] [http-moves this]
?. ?=(%bound +<.sign-arvo) ?. ?=(%bound +<.sign-arvo)
@ -148,23 +160,15 @@
|= response=client-response:iris |= response=client-response:iris
^- json ^- json
=, format =, format
=/ handler |= parsed-json=json =/ handler |= jon=json
?> ?=(%o -.parsed-json) =/ root ((ot:dejs ~[['root' same]]) jon)
=/ root=json (~(got by p.parsed-json) 'root') =/ stations ((ot:dejs ~[['stations' same]]) root)
?> ?=(%o -.root) =/ station ((ot:dejs ~[['station' (ar:dejs same)]]) stations)
=/ stations (~(got by p.root) 'stations') =/ abbr-and-name %- turn :- station |= item=json
?> ?=(%o -.stations)
=/ station=json (~(got by p.stations) 'station')
?> ?=(%a -.station)
=/ inner p.station
=/ abbr-and-name %- turn :- inner |= item=json
^- json ^- json
?> ?=(%o -.item) =/ [name=tape abbr=tape]
=/ name (~(got by p.item) 'name') ((ot:dejs ~[['name' sa:dejs] ['abbr' sa:dejs]]) item)
?> ?=(%s -.name) (pairs:enjs ~[name+(tape:enjs name) abbr+(tape:enjs abbr)])
=/ abbr (~(got by p.item) 'abbr')
?> ?=(%s -.abbr)
(pairs:enjs [name+s+p.name abbr+s+p.abbr ~])
(pairs:enjs [[%stations %a abbr-and-name] ~]) (pairs:enjs [[%stations %a abbr-and-name] ~])
(with-json-handler response handler) (with-json-handler response handler)
:: ::
@ -178,15 +182,58 @@
^- json ^- json
=, format =, format
=/ handler |= jon=json =/ handler |= jon=json
?> ?=(%o -.jon) =/ root=json ((ot:dejs ~[['root' same]]) jon)
=/ root=json (~(got by p.jon) 'root') =/ bsa=(list json) ((ot:dejs ~[['bsa' (ar:dejs same)]]) root)
?> ?=(%o -.root) (pairs:enjs [[%elevators %a bsa] ~])
=/ bsa=json (~(got by p.root) 'bsa')
?> ?=(%a -.bsa)
~& -.bsa
(pairs:enjs [[%elevators %a p.bsa] ~])
(with-json-handler response handler) (with-json-handler response handler)
:: ::
++ bart-api-routeplan
:: Documentation: http://api.bart.gov/docs/sched/depart.aspx
|= [from=tape to=tape hour=@ min=@ ispm=?]
^- request:http
=/ meridian ?:(ispm "pm" "am")
=/ minstr ?: =(min 0) "00"
?: (lte min 9) "0{<min>}"
"{<min>}"
=/ time "{<hour>}:{minstr}{meridian}"
=/ before 1
=/ after 3
=/ url (crip "{bart-api-url-base}/sched.aspx?cmd=depart&orig={from}&a={<after>}&b={<before>}&dest={to}&time={time}&key={bart-api-key}&json=y")
~& "Making BART API request to {<url>}"
=/ headers [['Accept' 'application/json']]~
[%'GET' url headers *(unit octs)]
++ parse-routeplan-response
|= response=client-response:iris
^- json
=, format
=/ handler
|= jon=json
=/ root=json ((ot:dejs [['root' same] ~]) jon)
=/ schedule=json ((ot:dejs [['schedule' same] ~]) root)
(pairs:enjs ~[[%routes schedule]])
(with-json-handler response handler)
++ poke-handle-json
|= jon=json
^- (list card)
~& jon
=, format
?. ?=(%o -.jon)
[~]
=/ [hour=@ min=@ ispm=? from-station=tape to-station=tape]
%.
jon
%: ot:dejs
['hour' ni:dejs]
['min' ni:dejs]
['isPM' bo:dejs]
['from' sa:dejs]
['to' sa:dejs]
~
==
=/ req (bart-api-routeplan from-station to-station hour min ispm)
=/ out *outbound-config:iris
[[%pass /routeplan %arvo %i %request req out] ~]
::
++ poke-handle-http-request ++ poke-handle-http-request
|= =inbound-request:eyre |= =inbound-request:eyre
^- simple-payload:http ^- simple-payload:http

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB