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 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 { 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.
@ -12,6 +22,63 @@ function isSundaySchedule(curTime) {
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() {
@ -65,17 +132,38 @@ class TimeScheduleWidget extends Component {
}
}
class RoutePlanner extends Component {
class RouteSearch extends Component {
constructor(props) {
super(props);
const now = new Date();
const hours = now.getHours();
this.state = {
fromStation: null,
toStation: null,
fromStation: "",
toStation: "",
depart: 'now',
min: now.getMinutes(),
hour: hours === 0 ? 12 : hours % 12,
isPM: hours >= 12
};
}
stationSearch() {
console.log("Searching");
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) {
@ -88,33 +176,142 @@ class RoutePlanner extends Component {
}
}
renderStationOptions() {
const stations = this.props.stations;
return _.map(stations, (station) => {
const abbr = station.abbr;
const name = station.name;
return <option key={abbr} value={abbr}>{name}</option>;
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
});
}
renderStationForm() {
return (<form name="bartSearch" onSubmit={this.stationSearch.bind(this)}>
From:
<select name="fromStation" value={this.state.fromStation || ""} onChange={this.changeStation.bind(this)}>
{ this.renderStationOptions() }
</select>
<br/>
To:
<select name="toStation" value={this.state.toStation || ""} onChange={this.changeStation.bind(this)}>
{ this.renderStationOptions() }
</select>
<input type="submit" value="Search"/>
</form>);
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 = isSundaySchedule(curTime) ? "BART-Map-Sunday.png" : "BART-Map-Weekday-Saturday.png";
const mapFilename = "BART-system-map.png";
const mapPath=`/~bartinfo/img/${mapFilename}`;
return (
@ -130,7 +327,11 @@ class RoutePlanner extends Component {
</div>
<div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}>
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>
@ -161,6 +362,7 @@ export class Root extends Component {
<RoutePlanner
curTime={new Date() }
stations={this.state.stations || []}
routes={this.state.routes}
/> } />
</div>
</BrowserRouter>

View File

@ -14,6 +14,11 @@ export class UpdateReducer {
if (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() {
/*
api.bind('/primary', 'PUT', api.authTokens.ship, 'bartinfo',
api.bind("/routes", "PUT", api.authTokens.ship, "bartinfo",
this.handleEvent.bind(this),
this.handleError.bind(this));
*/
api.bind("/elevators", "PUT", api.authTokens.ship, "bartinfo",
this.handleEvent.bind(this),

View File

@ -60,6 +60,10 @@
%+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request
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
[%pass /elevators %arvo %i %request req out]
[~[elevator-status-request] this]
?: ?=([%routes *] path)
[[~] this]
?: ?=([%http-response *] path)
`this
?. =(/ path)
@ -99,12 +105,18 @@
?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~])
[%give %fact ~[/bartstations] %json !>(update)]~
::
::
[%elevators *]
=/ value=json (parse-elevator-status-response:cc client-response.sign-arvo)
?> ?=(%o -.value)
=/ update=json (pairs:enjs:format [update+o+p.value ~])
[%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]
?. ?=(%bound +<.sign-arvo)
@ -148,23 +160,15 @@
|= response=client-response:iris
^- json
=, format
=/ handler |= parsed-json=json
?> ?=(%o -.parsed-json)
=/ root=json (~(got by p.parsed-json) 'root')
?> ?=(%o -.root)
=/ stations (~(got by p.root) 'stations')
?> ?=(%o -.stations)
=/ station=json (~(got by p.stations) 'station')
?> ?=(%a -.station)
=/ inner p.station
=/ abbr-and-name %- turn :- inner |= item=json
=/ handler |= jon=json
=/ root ((ot:dejs ~[['root' same]]) jon)
=/ stations ((ot:dejs ~[['stations' same]]) root)
=/ station ((ot:dejs ~[['station' (ar:dejs same)]]) stations)
=/ abbr-and-name %- turn :- station |= item=json
^- json
?> ?=(%o -.item)
=/ name (~(got by p.item) 'name')
?> ?=(%s -.name)
=/ abbr (~(got by p.item) 'abbr')
?> ?=(%s -.abbr)
(pairs:enjs [name+s+p.name abbr+s+p.abbr ~])
=/ [name=tape abbr=tape]
((ot:dejs ~[['name' sa:dejs] ['abbr' sa:dejs]]) item)
(pairs:enjs ~[name+(tape:enjs name) abbr+(tape:enjs abbr)])
(pairs:enjs [[%stations %a abbr-and-name] ~])
(with-json-handler response handler)
::
@ -178,15 +182,58 @@
^- json
=, format
=/ handler |= jon=json
?> ?=(%o -.jon)
=/ root=json (~(got by p.jon) 'root')
?> ?=(%o -.root)
=/ bsa=json (~(got by p.root) 'bsa')
?> ?=(%a -.bsa)
~& -.bsa
(pairs:enjs [[%elevators %a p.bsa] ~])
=/ root=json ((ot:dejs ~[['root' same]]) jon)
=/ bsa=(list json) ((ot:dejs ~[['bsa' (ar:dejs same)]]) root)
(pairs:enjs [[%elevators %a bsa] ~])
(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
|= =inbound-request:eyre
^- 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