Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2b14f7b605 |
35
README.md
35
README.md
@ -1,38 +1,5 @@
|
||||
BART (Bay Area Rapid Transit) landscape app for [Urbit](http://urbit.org).
|
||||
BART runner 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.
|
||||
|
||||
|
@ -3,16 +3,6 @@ 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.
|
||||
@ -22,63 +12,6 @@ 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() {
|
||||
@ -104,6 +37,50 @@ class ElevatorWidget extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// cf. https://github.com/urbit/urbit/blob/0c57e65b3871f1c40f1ecaf784722d4595c0d0ea/pkg/interface/chat/src/js/components/lib/ship-search.js
|
||||
class StationPicker extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchTerm: ""
|
||||
};
|
||||
this.search = this.search.bind(this);
|
||||
}
|
||||
|
||||
search(evt) {
|
||||
this.setState({searchTerm: evt.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const state = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="b--gray2 b--solid ba bg-white bg-gray0-d"
|
||||
>
|
||||
{props.from ? "From: " : "To: "}
|
||||
<textarea
|
||||
style={{ resize: 'none', maxWidth: '200px' }}
|
||||
className="ma2 pa2 b--gray4 ba b--solid w7 db bg-gray0-d white-d"
|
||||
rows={1}
|
||||
autocapitalise="none"
|
||||
autoFocus={
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
? false
|
||||
: true
|
||||
}
|
||||
placeholder="Station..."
|
||||
value={state.searchTerm}
|
||||
onChange={this.search}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeScheduleWidget extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
@ -132,38 +109,17 @@ class TimeScheduleWidget extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
class RouteSearch extends Component {
|
||||
class RoutePlanner 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
|
||||
fromStation: null,
|
||||
toStation: null,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
stationSearch() {
|
||||
console.log("Searching");
|
||||
}
|
||||
|
||||
changeStation(evt) {
|
||||
@ -176,142 +132,33 @@ class RouteSearch extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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>;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
renderStationForm() {
|
||||
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 name="fromStation" value={this.state.fromStation || ""} onChange={this.changeStation.bind(this)}>
|
||||
{ this.renderStationOptions() }
|
||||
</select>
|
||||
<br/>
|
||||
To:
|
||||
<select disabled={!receivedStations} name="toStation" value={this.state.toStation} onChange={this.changeStation.bind(this)}>
|
||||
{ getOptionsFromStations(receivedStations) }
|
||||
<select name="toStation" value={this.state.toStation || ""} onChange={this.changeStation.bind(this)}>
|
||||
{ this.renderStationOptions() }
|
||||
</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 mapFilename = isSundaySchedule(curTime) ? "BART-Map-Sunday.png" : "BART-Map-Weekday-Saturday.png";
|
||||
const mapPath=`/~bartinfo/img/${mapFilename}`;
|
||||
|
||||
return (
|
||||
@ -327,11 +174,11 @@ class RoutePlanner extends Component {
|
||||
</div>
|
||||
<div className="searchsidebar" style={{gridColumn: "2", gridRow: "2"}}>
|
||||
Search scheduled trains:
|
||||
<RouteSearch stations={this.props.stations} curTime={curTime} />
|
||||
<div>
|
||||
<StationPicker from/>
|
||||
<br/>
|
||||
<RouteResults routes={this.props.routes} />
|
||||
or see the official bart scheduler for a given station, date and time:
|
||||
<ScheduleWidget stations={this.props.stations}/>
|
||||
<StationPicker to/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -362,7 +209,6 @@ export class Root extends Component {
|
||||
<RoutePlanner
|
||||
curTime={new Date() }
|
||||
stations={this.state.stations || []}
|
||||
routes={this.state.routes}
|
||||
/> } />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
@ -14,11 +14,6 @@ export class UpdateReducer {
|
||||
if (elevators) {
|
||||
state.elevators = elevators;
|
||||
}
|
||||
|
||||
const routes = _.get(data, "routes", false);
|
||||
if (routes) {
|
||||
state.routes = routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,11 @@ export class Subscription {
|
||||
}
|
||||
|
||||
initializebartinfo() {
|
||||
api.bind("/routes", "PUT", api.authTokens.ship, "bartinfo",
|
||||
/*
|
||||
api.bind('/primary', '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),
|
||||
|
@ -60,10 +60,6 @@
|
||||
%+ 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)
|
||||
::
|
||||
==
|
||||
::
|
||||
@ -83,8 +79,6 @@
|
||||
=/ 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)
|
||||
@ -111,12 +105,6 @@
|
||||
?> ?=(%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)
|
||||
@ -160,15 +148,23 @@
|
||||
|= response=client-response:iris
|
||||
^- json
|
||||
=, format
|
||||
=/ 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
|
||||
=/ 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
|
||||
^- json
|
||||
=/ [name=tape abbr=tape]
|
||||
((ot:dejs ~[['name' sa:dejs] ['abbr' sa:dejs]]) item)
|
||||
(pairs:enjs ~[name+(tape:enjs name) abbr+(tape:enjs abbr)])
|
||||
?> ?=(%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 ~])
|
||||
(pairs:enjs [[%stations %a abbr-and-name] ~])
|
||||
(with-json-handler response handler)
|
||||
::
|
||||
@ -182,58 +178,15 @@
|
||||
^- json
|
||||
=, format
|
||||
=/ handler |= jon=json
|
||||
=/ root=json ((ot:dejs ~[['root' same]]) jon)
|
||||
=/ bsa=(list json) ((ot:dejs ~[['bsa' (ar:dejs same)]]) root)
|
||||
(pairs:enjs [[%elevators %a bsa] ~])
|
||||
?> ?=(%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] ~])
|
||||
(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
|
||||
|
BIN
urbit/app/bartinfo/img/BART-Map-Sunday.png
Normal file
BIN
urbit/app/bartinfo/img/BART-Map-Sunday.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 MiB |
BIN
urbit/app/bartinfo/img/BART-Map-Weekday-Saturday.png
Normal file
BIN
urbit/app/bartinfo/img/BART-Map-Weekday-Saturday.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 MiB |
Binary file not shown.
Before Width: | Height: | Size: 5.3 MiB |
Loading…
Reference in New Issue
Block a user